diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index c8aa2ebe5f..8d145e39c4 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,8 +3,6 @@ name: Docker Image CI on: push: branches: [ master ] - pull_request: - branches: [ master ] jobs: diff --git a/.github/workflows/latest-bitcoind.yml b/.github/workflows/latest-bitcoind.yml index 7c687374ee..c768af0f68 100644 --- a/.github/workflows/latest-bitcoind.yml +++ b/.github/workflows/latest-bitcoind.yml @@ -5,6 +5,9 @@ on: # Run at midnight on Sunday and Wednesday. - cron: '0 0 * * 0,3' +permissions: + contents: read + jobs: regression-tests: @@ -18,7 +21,7 @@ jobs: path: bitcoin - name: Install bitcoind dependencies - run: sudo apt-get install build-essential libtool autotools-dev automake pkg-config bsdmainutils python3 libssl-dev libevent-dev libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev libminiupnpc-dev libzmq3-dev libqt5gui5 libqt5core5a libqt5dbus5 qttools5-dev qttools5-dev-tools libprotobuf-dev protobuf-compiler git libsqlite3-dev ccache + run: sudo apt-get install build-essential libtool autotools-dev automake pkg-config bsdmainutils python3 libevent-dev libboost-dev libminiupnpc-dev libnatpmp-dev libzmq3-dev libsqlite3-dev systemtap-sdt-dev working-directory: ./bitcoin - name: Autogen bitcoind diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a48b058d8..1191b29621 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 steps: - uses: actions/checkout@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6740717247..96aa2fb1d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,9 @@ You can also use Github issues for [feature requests](https://github.com/acinq/e - [Lightning Network Whitepaper](https://lightning.network/lightning-network-paper.pdf) - [Deployable Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) - [Understanding the Lightning Network](https://bitcoinmagazine.com/articles/understanding-the-lightning-network-part-building-a-bidirectional-payment-channel-1464710791) -- [Lightning Network Specification](https://github.com/lightningnetwork/lightning-rfc) +- [Lightning Network Specification](https://github.com/lightning/bolts) - [High Level Lightning Network Specification](https://medium.com/@rusty_lightning/the-bitcoin-lightning-spec-part-1-8-a7720fb1b4da) +- [t-bast's in-depth articles about the Lightning Network](https://github.com/t-bast/lightning-docs/) ## Recommended Skillset @@ -82,6 +83,12 @@ The reason for this is two fold: it makes it easier for the reviewer to see what made between versions (since Github doesn't easily show prior versions) and it makes it easier on the PR author as they can set it to auto-squash the fixup commits on rebase. +When you have addressed a pull request comment, you should respond to it and point the reviewer +to the commit where you fixed it. A simple "This is fixed in " is enough. Do not +mark the comment as resolved: let the reviewer verify your commit, they will mark the comment +resolved if they agree with the fix. This makes it easier for reviewers to check that all their +comments have been addressed. + It's recommended to take great care in writing tests and ensuring the entire test suite has a stable successful outcome; eclair uses continuous integration techniques and having a stable build helps the reviewers with their job. @@ -140,6 +147,14 @@ If your contribution is adding a new dependency, please detail: Contributions that add new dependencies may take longer to approve because a detailed audit of the dependency may be required. +### Testing + +Your code should be tested. We use ScalaTest as a testing framework. + +ScalaTest's approach is to parallelize on test suites rather than individual tests, therefore it is +recommended to keep the execution time of each test suite under one minute and split tests across +multiple smaller suites if needed. + ### IntelliJ Tips If you're using [IntelliJ](https://www.jetbrains.com/idea/), here are some useful commands: diff --git a/README.md b/README.md index 4ce0fa7672..6d5d8cd891 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. -This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning), [lnd](https://github.com/LightningNetwork/lnd), [electrum](https://github.com/spesmilo/electrum/), and [rust-lightning](https://github.com/rust-bitcoin/rust-lightning). +This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightning/bolts). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning), [lnd](https://github.com/LightningNetwork/lnd), [electrum](https://github.com/spesmilo/electrum/), and [rust-lightning](https://github.com/rust-bitcoin/rust-lightning). --- @@ -25,6 +25,7 @@ This software follows the [Lightning Network Specifications (BOLTs)](https://git * [Docker](#docker) * [Plugins](#plugins) * [Testnet usage](#testnet-usage) +* [Tools](#tools) * [Resources](#resources) --- @@ -43,10 +44,10 @@ For more information please visit the [API documentation website](https://acinq. ## Documentation -Please visit our [docs](./docs) and [wiki](https://github.com/acinq/eclair/wiki) to find detailed instructions on how to configure your -node, connect to other nodes, open channels, send and receive payments, and more advanced scenario. +Please visit our [docs](./docs) folder to find detailed instructions on how to [configure](./docs/Configure.md) your +node, connect to other nodes, open channels, send and receive payments, and help with more advanced scenarios. -You will find detailed guides and frequently asked questions there. +You will also find detailed [guides](./docs/Guides.md) and [frequently asked questions](./docs/FAQ.md) there. ## Installation @@ -95,7 +96,7 @@ Then download our latest [release](https://github.com/ACINQ/eclair/releases), un eclair-node--/bin/eclair-node.sh ``` -You can then control your node via the [eclair-cli](https://github.com/ACINQ/eclair/wiki/Usage) or the [API](https://github.com/ACINQ/eclair/wiki/API). +You can then control your node via [eclair-cli](./docs/Usage.md) or the [API](./docs/API.md). :warning: Be careful when following tutorials/guides that may be outdated or incomplete. You must thoroughly read the official eclair documentation before running your own node. @@ -168,7 +169,7 @@ eclair-node--/bin/eclair-node.sh -Declair.datadir=/tmp/node1 ### Logging -Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a different configuration, and override the internal logback.xml, run: +Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a [different configuration](./docs/Logging.md), and override the internal logback.xml, run: ```shell eclair-node--/bin/eclair-node.sh -Dlogback.configurationFile=/path/to/logback-custom.xml @@ -176,7 +177,14 @@ eclair-node--/bin/eclair-node.sh -Dlogback.configurationFile ### Backup -The files that you need to backup are located in your data directory. You must backup: +You need to backup: +- your Bitcoin Core wallet +- your Eclair channels + +For Bitcoin Core, you need to backup the wallet file for the wallet that Eclair is using. You only need to do this once, when the wallet is +created. See [Managing Wallets](https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md) in the Bitcoin Core documentation for more information. + +For Eclair, the files that you need to backup are located in your data directory. You must backup: * your seeds (`node_seed.dat` and `channel_seed.dat`) * your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) @@ -198,7 +206,8 @@ before copying that file to your final backup location. ## Docker -A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. +A [Dockerfile](Dockerfile) x86_64 image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. +For arm64 platforms you can use an [arm64 Dockerfile](contrib/arm64v8.Dockerfile) to build your own arm64 container. You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`. @@ -212,7 +221,7 @@ If you want to persist the data directory, you can make the volume to your host docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair ``` -If you enabled the API you can check the status of eclair using the command line tool: +If you enabled the API you can check the status of Eclair using the command line tool: ```shell docker exec eclair-cli -p foobar getinfo @@ -232,14 +241,14 @@ eclair-node--/bin/eclair-node.sh ### Non-exhaustive plugins list -Here are some plugins created by the eclair community. +Here are some plugins created by the Eclair community. If you need support for these plugins, head over to their respective github repository. -* [Telegram Bot for eclair alerts](https://github.com/engenegr/eclair-alarmbot-plugin) +* [Telegram Bot for Eclair alerts](https://github.com/engenegr/eclair-alarmbot-plugin) ## Testnet usage -Eclair is configured to run on mainnet by default, but you can still run it on testnet (or regtest): start your Bitcoin Node in +Eclair is configured to run on mainnet by default, but you can still run it on testnet (or regtest): start your Bitcoin node in testnet mode (add `testnet=1` in `bitcoin.conf` or start with `-testnet`), and change Eclair's chain parameter and Bitcoin RPC port: ```conf @@ -248,7 +257,7 @@ eclair.bitcoind.rpcport=18332 ``` You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network specific, -so you can easily run your bitcoin node on both mainnet and testnet. For example you could use: +so you can easily run your Bitcoin node on both mainnet and testnet. For example you could use: ```conf server=1 @@ -265,6 +274,11 @@ zmqpubhashblock=tcp://127.0.0.1:29001 zmqpubrawtx=tcp://127.0.0.1:29001 ``` +## Tools + +* [Demo Shop](https://starblocks.acinq.co/) - an example testnet Lightning web shop. +* [Network Explorer](https://explorer.acinq.co/) - a Lightning network visualization tool. + ## Resources * [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja diff --git a/contrib/arm64v8.Dockerfile b/contrib/arm64v8.Dockerfile new file mode 100644 index 0000000000..c61474481b --- /dev/null +++ b/contrib/arm64v8.Dockerfile @@ -0,0 +1,65 @@ +FROM eclipse-temurin:11-jdk-jammy as BUILD + +# Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache +# We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image +# this was meant to make the image smaller, but we use multi-stage build so we don't care +RUN apt update && apt install -y curl tar bash + +ARG MAVEN_VERSION=3.6.3 +ARG USER_HOME_DIR="/root" +ARG SHA=c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0 +ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries + +RUN mkdir -p /usr/share/maven /usr/share/maven/ref \ + && curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ + && echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \ + && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \ + && rm -f /tmp/apache-maven.tar.gz \ + && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn + +ENV MAVEN_HOME /usr/share/maven +ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2" + +# Let's fetch eclair dependencies, so that Docker can cache them +# This way we won't have to fetch dependencies again if only the source code changes +# The easiest way to reliably get dependencies is to build the project with no sources +WORKDIR /usr/src +COPY pom.xml pom.xml +COPY eclair-core/pom.xml eclair-core/pom.xml +COPY eclair-front/pom.xml eclair-front/pom.xml +COPY eclair-node/pom.xml eclair-node/pom.xml +COPY eclair-node/modules/assembly.xml eclair-node/modules/assembly.xml +RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala +# Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core +RUN mvn install -pl eclair-node -am +RUN mvn clean + +# Only then do we copy the sources +COPY . . + +# And this time we can build in offline mode, specifying 'notag' instead of git commit +RUN mvn package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.commit.id.abbrev=notag -o +# It might be good idea to run the tests here, so that the docker build fail if the code is bugged + +# We currently use a debian image for runtime because of some jni-related issue with sqlite +FROM openjdk:11.0.4-jre-slim +WORKDIR /app + +# install jq for eclair-cli +RUN apt-get update && apt-get install -y bash jq curl unzip + +# copy and install eclair-cli executable +COPY --from=BUILD /usr/src/eclair-core/eclair-cli . +RUN chmod +x eclair-cli && mv eclair-cli /sbin/eclair-cli + +# we only need the eclair-node.zip to run +COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.zip ./eclair-node.zip +RUN unzip eclair-node.zip && mv eclair-node-* eclair-node && chmod +x eclair-node/bin/eclair-node.sh + +ENV ECLAIR_DATADIR=/data +ENV JAVA_OPTS= + +RUN mkdir -p "$ECLAIR_DATADIR" +VOLUME [ "/data" ] + +ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh "-Declair.datadir=${ECLAIR_DATADIR}" diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000000..cd3484d69a --- /dev/null +++ b/docs/API.md @@ -0,0 +1,43 @@ +# API + +Eclair can setup a JSON API on the `eclair.api.port` port set in the [configuration](./Configure.md). You first need to enable it: + +``` +eclair.api.enabled=true +eclair.api.password=changeit +``` + +:rotating_light: **Attention:** Eclair's API should NOT be accessible from the outside world (similarly to Bitcoin Core API). + +## Payment notification + +Eclair accepts websocket connection on `ws://localhost:/ws`, and emits a message containing the payment hash of a payment when receiving a payment. + +## API calls + +This API exposes all the necessary methods to read the current state of the node, open/close channels and send/receive payments. For the full documentation please visit https://acinq.github.io/eclair + +#### Example: open a channel + +Your node listens on 8081. You want to open a 140 mBTC channel with `endurance.acinq.co` on Testnet with a 30 mBTC `push` + +1/ connect to the node with the URI: + +```shell +curl -X POST \ + -u :api_password \ + -F uri="03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@endurance.acinq.co:9735" \ + http://127.0.0.1:8081/connect +``` + +2/ Open a channel with this node + +```shell +curl -X POST \ + -u :api_password \ + -F nodeId=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134 \ + -F fundingSatoshis=14000000 \ + http://127.0.0.1:8081/open +``` + +Feeling tired of writing all these `curls`? Good news, a CLI bash file is available. It uses this API to interact with your node. More information about `eclair-cli` is [here](./Usage.md). \ No newline at end of file diff --git a/docs/Architecture.md b/docs/Architecture.md index c7f74cafba..ce53003a99 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -106,13 +106,13 @@ Here is a high-level view of the hierarchy of some of the main actors in the sys And a short description of each actor's role: - Switchboard: creates and deletes peers -- Peer: p2p connection to another lightning node (standard lightning messages described in [Bolt 1](https://github.com/lightningnetwork/lightning-rfc/blob/master/01-messaging.md)) -- Channel: channel with another lightning node ([Bolt 2](https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md)) +- Peer: p2p connection to another lightning node (standard lightning messages described in [Bolt 1](https://github.com/lightning/bolts/blob/master/01-messaging.md)) +- Channel: channel with another lightning node ([Bolt 2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md)) - Register: maps channel IDs to actors (provides a clean boundary between channel and payment components) - PaymentInitiator: entry point for sending payments - Relayer: entry point for relaying payments - PaymentHandler: entry point for receiving payments -- Router: p2p gossip and the network graph ([Bolt 7](https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md)) +- Router: p2p gossip and the network graph ([Bolt 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md)) Actors have two ways of communicating: @@ -128,7 +128,7 @@ Let's dive into a few payment scenarios to show which actors are involved. When we send a payment: - we run a path-finding algorithm (`Router`) -- we split the payment into smaller chunks if [MPP](https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#basic-multi-part-payments) is used (`MultiPartPaymentLifecycle`) +- we split the payment into smaller chunks if [MPP](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) is used (`MultiPartPaymentLifecycle`) - we retry with alternative routes in some failure cases and record failing channels/payments (`PaymentLifecycle`) - we add HTLCs to some of our channels @@ -175,7 +175,7 @@ When we relay a payment: - htlcs are forwarded by channels to the relayer - the relayer identifies the type of relay requested and delegates work to a channel relayer or a node relayer -- if a node relayer is used ([trampoline payments](https://github.com/lightningnetwork/lightning-rfc/pull/829)): +- if a node relayer is used ([trampoline payments](https://github.com/lightning/bolts/pull/829)): - incoming htlcs are validated by a payment handler (similar to the flow to receive payments) - outgoing htlcs are sent out (similar to the flow to send payments) diff --git a/docs/Cluster.md b/docs/Cluster.md index 2ad0d746ea..e93e42ff25 100644 --- a/docs/Cluster.md +++ b/docs/Cluster.md @@ -37,11 +37,13 @@ The goal is to offload your node from connection and routing table management: ### Prerequisite -You already have a node up and running in a standalone setup (with Bitcoin Core properly configured, etc.). +You already have a lightning node up and running in a standalone setup (with Bitcoin Core properly configured, etc.). You know what your `node id` is. -In the following, what we previously called `eclair-node` will be called `backend`. It is to be launched, configured and backed-up exactly like in a standalone setup. +Conventions used in this document: +- what we previously called `eclair-node` will be called `backend`. It is to be launched, configured and backed-up exactly like in a standalone setup. +- `node` refer to *akka cluster nodes*, not to be confused with lightning nodes. Together, all *cluster nodes* form a single logical *lighting node*. ### Minimal/Demo setup @@ -72,7 +74,7 @@ NB: we override the ports, otherwise they would conflict since in this example e ### Production setup In production you should: -- run multiple `frontend`s +- run multiple `frontend` servers - run one app per server - enable `tcp-tls` to encrypt communications between members of the cluster with your own generated certificate (see below) - use a load balancer to hide all your `frontend` servers under the same ip address @@ -86,31 +88,47 @@ We use a self-signed certificate, which offers a good compromise. More advanced > Have a single set of keys and a single certificate for all nodes and disable hostname checking > - The single set of keys and the single certificate is distributed to all nodes. The certificate can be self-signed as it is distributed both as a certificate for authentication but also as the trusted certificate. > - If the keys/certificate are lost, someone else can connect to your cluster. -> - Adding nodes to the cluster is simple as the key material can be deployed / distributed to the new node. +> - Adding nodes to the cluster is simple as the key material can be deployed / distributed to the new cluster node. Generate a self-signed certificate (set a strong password): ```shell $ keytool -genkeypair -v \ -keystore akka-cluster-tls.jks \ -dname "O=ACME, C=FR" \ - -keypass:env \ - -storepass:env \ + -keypass \ + -storepass \ -keyalg RSA \ -keysize 4096 \ -validity 9999 ``` -Copy the resulting certificate to your `.eclair` directory: +Copy the resulting certificate to the `.eclair` directory on your backend node and all your frontend nodes: ```shell $ cp akka-cluster-tls.jks ~/.eclair ``` +Add this to `eclair.conf` on all your frontend nodes: +``` +akka.remote.artery.transport = "tls-tcp" +``` -Add this to your `eclair.conf`: +#### Run cluster nodes on separate servers + +Start all your frontend nodes with the following environment variables: +* BACKEND_IP set to the IP address of your backend node +* LOCAL_IP set to the IP address of this frontend node (this is typically a private IP address, reachable from your backend node) +* NODE_PUB_KEY set to your node public key +* AKKA_TLS_PASSWORD set to the password of your Akka certificate + +Add this to `eclair.conf` on your backend node: ``` -AKKA_TLS_PASSWORD= akka.remote.artery.transport = "tls-tcp" +akka.remote.artery.canonical.hostname="ip-of-this-backend-node" +akka.cluster.seed-nodes=["akka://eclair-node@ip-of-this-backend-node:25520"] ``` +Start your backend node with the following environment variables: +* AKKA_TLS_PASSWORD set to the password of your Akka certificate + ### AWS Deployment For convenience, we provide a prebuilt AWS Beanstalk bundle for the `frontend` (choose a WebServer environment type, and Java platform). diff --git a/docs/Configure.md b/docs/Configure.md index bbf9b06f52..754abfd404 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -19,8 +19,7 @@ ## Configuration file The configuration file for eclair is named `eclair.conf`. It is located in the data directory, which is `~/.eclair` by -default. Note that eclair won't create a configuration file by itself: if you want to change eclair's configuration, you -need to **actually create the configuration file first**. The encoding must be UTF-8. +default (on Windows it is `C:\Users\YOUR_NAME\.eclair`). Note that eclair won't create a configuration file by itself: if you want to change eclair's configuration, you need to **actually create the configuration file first**. The encoding must be UTF-8. ```sh # this is the default data directory, it will be created at eclair first startup @@ -100,27 +99,55 @@ It's usually risky to activate non-default features or disable default features: Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob. When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with. -This is done with the `override-features` configuration parameter in your `eclair.conf`: +This is done with the `override-init-features` configuration parameter in your `eclair.conf`: ```conf -eclair.override-features = [ +eclair.features { + var_onion_optin = mandatory + payment_secret = mandatory + option_support_large_channel = optional + option_static_remotekey = optional + option_channel_type = optional +} +eclair.override-init-features = [ { - nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + nodeId = "" features { option_support_large_channel = disabled - option_static_remotekey = optional + option_static_remotekey = mandatory + option_anchors_zero_fee_htlc_tx = optional } }, { - nodeId = "" + nodeId = "" features { - option_static_remotekey = optional - option_support_large_channel = optional + option_support_large_channel = mandatory + option_static_remotekey = disabled + option_channel_type = mandatory } }, ] ``` +In this example, the features that will be activated with Alice are: + +```conf +var_onion_optin = mandatory +payment_secret = mandatory +option_static_remotekey = mandatory +option_anchors_zero_fee_htlc_tx = optional +option_channel_type = optional +``` + +The features that will be activated with Bob are: + +```conf +var_onion_optin = mandatory +payment_secret = mandatory +option_support_large_channel = mandatory +option_channel_type = mandatory +``` + ## Customize feerate tolerance In order to secure your channels' funds against attacks, your eclair node keeps an up-to-date estimate of on-chain feerates (based on your Bitcoin node's estimations). diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000000..802216d480 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,18 @@ +# FAQ + +## What does it mean for a channel to be "enabled" or "disabled" ? + +A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled. + +Suppose you're A, with the following setup: +``` +A ---ab--> B --bc--> C +``` +And node C goes down. B will publish a channel update for channel `bc` with the `disable` bit set. +There are other cases when a channel becomes disabled, for example when its balance goes below reserve... + +Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific). + +## How should you stop an Eclair node ? + +To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres. diff --git a/docs/Features.md b/docs/Features.md new file mode 100644 index 0000000000..57874fe205 --- /dev/null +++ b/docs/Features.md @@ -0,0 +1,53 @@ +# Customize Features + +Eclair ships with a set of features that are activated by default, and some experimental or optional features that can be activated by users. +The list of supported features can be found in the [reference configuration](https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/resources/reference.conf). + +To enable a non-default feature, you simply need to add the following to your `eclair.conf`: + +```conf +eclair.features { + official_feature_name = optional|mandatory +} +``` + +For example, to activate `option_static_remotekey`: + +```conf +eclair.features { + option_static_remotekey = optional +} +``` + +Note that you can also disable some default features: + +```conf +eclair.features { + initial_routing_sync = disabled +} +``` + +It's usually risky to activate non-default features or disable default features: make sure you fully understand a feature (and the current implementation status, detailed in the release notes) before doing so. + +Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob. When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with. + +This is done with the `override-features` configuration parameter in your `eclair.conf`: + +```conf +eclair.override-features = [ + { + nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + features { + initial_routing_sync = disabled + option_static_remotekey = optional + } + }, + { + nodeId = "" + features { + option_static_remotekey = optional + option_support_large_channel = optional + } + }, +] +``` \ No newline at end of file diff --git a/docs/Guides.md b/docs/Guides.md new file mode 100644 index 0000000000..6f7a19d0a9 --- /dev/null +++ b/docs/Guides.md @@ -0,0 +1,14 @@ +# Guides + +This section contains how-to guides for more advanced scenarios: + +* [Customize Logging](./Logging.md) +* [Customize Features](./Features.md) +* [Use Tor with Eclair](./Tor.md) +* [Multipart Payments](./MultipartPayments.md) +* [Trampoline Payments](./TrampolinePayments.md) +* [Monitoring Eclair](./Monitoring.md) +* [PostgreSQL Configuration](./PostgreSQL.md) +* [Perform Circular Rebalancing](./CircularRebalancing.md) +* [Clusterize your Eclair node](./Cluster.md) +* [Architecture of Eclair code](./Architecture.md) \ No newline at end of file diff --git a/docs/Logging.md b/docs/Logging.md new file mode 100644 index 0000000000..a4f7bd710a --- /dev/null +++ b/docs/Logging.md @@ -0,0 +1,60 @@ +# Logging + +### Customize Logging + +Eclair uses [logback](https://logback.qos.ch/) for logging. +By default the logging level is set to `INFO` for the whole application. +To use a different configuration, and override the internal `logback.xml`, run: + +```shell +eclair-node.sh -Dlogback.configurationFile=/path/to/logback-custom.xml +``` + +If you want parts of the application to log at the `DEBUG` logging level, you will also need to override `akka.loglevel`: + +```shell +eclair-node.sh -Dakka.loglevel=DEBUG -Dlogback.configurationFile=/path/to/logback-custom.xml +``` + +You can find the default `logback.xml` file [here](https://github.com/ACINQ/eclair/blob/master/eclair-node/src/main/resources/logback.xml). It logs everything more serious than `INFO` to a rolling file. + +If you want to debug an issue, you can change the root logger's log level to `DEBUG`: + +```xml + + + +``` + +This setting will produce a lot of logs. If you are investigating an issue with payments, you can instead turn on `DEBUG` logging only for payments-related components by adding a new `logger` before the `root`: + +```xml + + + + + +``` + +On the contrary, if you want a component to emit less logs, you can set its log level to `WARN` or `ERROR`: + +```xml + + + + + +``` + +To figure out the `name` you should use for the `logger` element, you need to look at the hierarchy of the source code in Eclair's [Github repository](https://github.com/ACINQ/eclair). You can even configure loggers for each specific class you're interested in: + +```xml + + + + + + +``` + +For more advanced use-cases, please see logback's [official documentation](https://logback.qos.ch/documentation.html). \ No newline at end of file diff --git a/docs/Monitoring.md b/docs/Monitoring.md index b914a3c776..086cc5c6de 100644 --- a/docs/Monitoring.md +++ b/docs/Monitoring.md @@ -33,6 +33,7 @@ kamon { } ``` + When starting eclair, you should enable the Kanela agent: ```sh @@ -42,13 +43,53 @@ eclair.sh -with-kanela Your eclair node will start exporting monitoring data to Kamon. You can then start creating dashboards, graphs and alerts directly on Kamon's website. -## Enabling monitoring with a different backend +## Enabling monitoring with Prometheus Kamon supports many other monitoring [backends](https://kamon.io/docs/latest/reporters/). This can be useful for nodes that don't want to export any data to third-party servers. -No specific work has been done yet in eclair to support these backends. If you'd like to use them, -don't hesitate to ask around or send a PR. +Eclair currently supports exporting metrics to [Prometheus](https://kamon.io/docs/latest/reporters/prometheus/). +To enable monitoring with Prometheus, add the following to your `eclair.conf`: + +```config +eclair.enable-kamon=true + +// The Kamon APM reporter is enabled by default, but it won't work with Prometheus, so we disable it. +kamon.modules { + apm-reporter { + enabled = false + } +} + +kamon { + prometheus { + start-embedded-http-server = yes + embedded-server { + hostname = 0.0.0.0 + port = + } + } +} +``` + +You should then configure your Prometheus process to scrape metrics from the exposed http server. +* Download Prometheus [here](https://prometheus.io/download/). +* Add the following configuration to the `prometheus.yml` file (see the [Prometheus documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/) for more details) + +```prometheus.yml +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + +scrape_configs: + - job_name: 'eclair' + static_configs: + - targets: [''] + ``` + +Eclair provides many [Grafana](https://grafana.com/) dashboards to help you monitor your lightning node, which you can find in the `monitoring` folder of this repository. Follow the [Grafana documentation](https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard) to import these dashboards and create new ones if necessary. + +Note: do not forget to add `Prometheus` as a data source in grafana (see [grafana documentation](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-data-source) for more details) + ## Example metrics @@ -60,3 +101,4 @@ metrics are just a small sample of all the metrics we provide: * Number of connected peers * Bitcoin wallet balance * Various metrics about the public graph (nodes, channels, updates, etc) + diff --git a/docs/MultipartPayments.md b/docs/MultipartPayments.md new file mode 100644 index 0000000000..878553e935 --- /dev/null +++ b/docs/MultipartPayments.md @@ -0,0 +1,80 @@ +# Multipart Payments + +Eclair started supporting [multi-part payments](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) in v0.3.3. + +## Receiving multi-part payments + +Once the feature is activated, your eclair node will generate invoices that can be paid using MPP. +If the sender also supports MPP, your node will accept the payment. +It's as simple as that! + +Here is how you generate an invoice: + +```sh +eclair-cli createinvoice --amountMsat=42000000 --description="MPP is #reckless" +``` + +## Sending multi-part payments + +### With the built-in splitting algorithm + +If you try paying an invoice that supports MPP, eclair will automatically split the payment (if needed). + +You just need to use the `payinvoice` command, nothing complicated here: + +```sh +eclair-cli payinvoice --invoice=lnbc11pdkmqhupp5n2e... +``` + +However, we are still experimenting with various algorithms to efficiently split payments. +You may find that the resulting split looks less optimal that what you expected, in which case the next section is for you! + +### With your own splitting algorithm + +The CLI allows you to fully control how your payment is split and sent. This is a good way to start experimenting with MPP. + +Let's imagine that the network looks like this: + +```txt + +------- Bob -------+ + | | +Alice Dave + | | + +------- Carol -----+ + +``` + +Dave has generated an MPP invoice for 400000 msat: `lntb1500n1pwxx94fp...`. + +You want to send 150000 msat through Bob and 250000 msat through Carol. + +Initiate the payment by sending the first part: + +```sh +eclair-cli sendtoroute --amountMsat=150000 --nodeIds=$ALICE_ID,$BOB_ID,$DAVE_ID --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +This will return some identifiers that must be used for the other parts: + +```json +{ + "paymentId": "4e8f2440-dbfd-4e76-bb45-a0647a966b2a", + "parentId": "cd083b31-5939-46ac-bf90-8ac5b286a9e2" +} +``` + +The `parentId` is important: this is the identifier used to link the parts together. + +Now that you have those, you can send the second part: + +```sh +eclair-cli sendtoroute --parentId=cd083b31-5939-46ac-bf90-8ac5b286a9e2 --amountMsat=250000 --nodeIds=$ALICE_ID,$CAROL_ID,$DAVE_ID --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +You can then check the status of the payment with the `getsentinfo` command: + +```sh +eclair-cli getsentinfo --id=cd083b31-5939-46ac-bf90-8ac5b286a9e2 +``` + +Once Dave accepts the HTLCs you should see all the details about the payment success (preimage, route, fees, etc). diff --git a/docs/PostgreSQL.md b/docs/PostgreSQL.md index e312da36de..315d45b3f8 100644 --- a/docs/PostgreSQL.md +++ b/docs/PostgreSQL.md @@ -111,4 +111,34 @@ Eclair stores the latest database settings in the `${data-dir}/last_jdbcurl` fil The node operator can force Eclair to accept new database connection settings by removing the `last_jdbcurl` file. - \ No newline at end of file + +### Migrating from Sqlite to Postgres + +Eclair supports migrating your existing node from Sqlite to Postgres. Note that the opposite (from Postgres to Sqlite) is not supported. + +:warning: Once you have migrated from Sqlite to Postgres there is no going back! + +To migrate from Sqlite to Postgres, follow these steps: +1. Stop Eclair +2. Edit `eclair.conf` + 1. Set `eclair.db.postgres.*` as explained in the section [Connection Settings](#connection-settings). + 2. Set `eclair.db.driver=dual-sqlite-primary`. This will make Eclair use both databases backends. All calls to sqlite will be replicated in postgres. + 3. Set `eclair.db.dual.migrate-on-restart=true`. This will make Eclair migrate the data from Sqlite to Postgres at startup. + 4. Set `eclair.db.dual.compare-on-restart=true`. This will make Eclair compare Sqlite and Postgres at startup. The result of the comparison is displayed in the logs. +3. Delete the file `~/.eclair/last_jdbcurl`. The purpose of this file is to prevent accidental change in the database backend. +4. Start Eclair. You should see in the logs: + 1. `migrating all tables...` + 2. `migration complete` + 3. `comparing all tables...` + 4. `comparison complete identical=true` (NB: if `identical=false`, contact support) +5. Eclair should then finish startup and operate normally. Data has been migrated to Postgres, and Sqlite/Postgres will be maintained in sync going forward. +6. Edit `eclair.conf` and set `eclair.db.dual.migrate-on-restart=false` but do not restart Eclair yet. +7. We recommend that you leave Eclair in dual db mode for a while, to make sure that you don't have issues with your new Postgres database. This a good time to set up [Backups and replication](#backups-and-replication). +8. After some time has passed, restart Eclair. You should see in the logs: + 1. `comparing all tables...` + 2. `comparison complete identical=true` (NB: if `identical=false`, contact support) +9. At this point we have confidence that the Postgres backend works normally, and we are ready to drop Sqlite for good. +10. Edit `eclair.conf` + 1. Set `eclair.db.driver=postgres` + 2. Set `eclair.db.dual.compare-on-restart=false` +11. Restart Eclair. From this moment, you cannot go back to Sqlite! If you try to do so, Eclair will refuse to start. \ No newline at end of file diff --git a/docs/Tor.md b/docs/Tor.md index 95ffea146d..377e43a610 100644 --- a/docs/Tor.md +++ b/docs/Tor.md @@ -1,5 +1,7 @@ ## How to Use Tor with Eclair +Current supported version of Tor is 0.3.3.6 or higher. + ### Installing Tor on your node #### Linux: @@ -100,21 +102,7 @@ eclair-cli getinfo ``` Eclair saves the Tor endpoint's private key in `~/.eclair/tor.dat`, so that it can recreate the endpoint address after a restart. If you remove the private key Eclair will regenerate the endpoint address. - -There are two possible values for `protocol-version`: - -``` -eclair.tor.protocol-version = "v3" -``` - -value | description ---------|--------------------------------------------------------- - v2 | set up a Tor hidden service version 2 end point - v3 | set up a Tor hidden service version 3 end point (default) -Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports -both v2 and v3 addresses. - For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`: ``` eclair.server.binding-ip = "127.0.0.1" diff --git a/docs/TrampolinePayments.md b/docs/TrampolinePayments.md new file mode 100644 index 0000000000..62ffa3b1cd --- /dev/null +++ b/docs/TrampolinePayments.md @@ -0,0 +1,68 @@ +# Trampoline Payments + +Eclair started supporting [trampoline payments](https://github.com/lightning/bolts/pull/829) in v0.3.3. + +It is disabled by default, as it is still being reviewed for spec acceptance. However, if you want to experiment with it, here is what you can do. + +First of all, you need to activate the feature for any node that will act as s trampoline node. Update your `eclair.conf` with the following values: + +```conf +eclair.trampoline-payments-enable=true +``` + +## Sending trampoline payments + +The CLI allows you to fully control how your payment is split and sent. This is a good way to start experimenting with Trampoline. + +Let's imagine that the network looks like this: + +```txt +Alice -----> Bob -----> Carol -----> Dave +``` + +Where Bob is a trampoline node and Alice, Carol and Dave are "normal" nodes. + +Let's imagine that Dave has generated an MPP invoice for 400000 msat: `lntb1500n1pwxx94fp...`. +Alice wants to pay that invoice using Bob as a trampoline. +To spice things up, Alice will use MPP between Bob and her, splitting the payment in two parts. + +Initiate the payment by sending the first part: + +```sh +eclair-cli sendtoroute --amountMsat=150000 --nodeIds=$ALICE_ID,$BOB_ID --trampolineNodes=$BOB_ID,$DAVE_ID --trampolineFeesMsat=100000 --trampolineCltvExpiry=450 --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +Note the `trampolineFeesMsat` and `trampolineCltvExpiry`. At the moment you have to estimate those yourself. If the values you provide are too low, Bob will send an error and you can retry with higher values. In future versions, we will automatically fill those values for you. + +The command will return some identifiers that must be used for the other parts: + +```json +{ + "paymentId": "4e8f2440-dbfd-4e76-bb45-a0647a966b2a", + "parentId": "cd083b31-5939-46ac-bf90-8ac5b286a9e2", + "trampolineSecret": "9e13d1b602496871bb647b48e8ff8f15a91c07affb0a3599e995d470ac488715" +} +``` + +The `parentId` is important: this is the identifier used to link the MPP parts together. + +The `trampolineSecret` is also important: this is what prevents a malicious trampoline node from stealing money. + +Now that you have those, you can send the second part: + +```sh +eclair-cli sendtoroute --amountMsat=250000 --parentId=cd083b31-5939-46ac-bf90-8ac5b286a9e2 --trampolineSecret=9e13d1b602496871bb647b48e8ff8f15a91c07affb0a3599e995d470ac488715 --nodeIds=$ALICE_ID,$BOB_ID --trampolineNodes=$BOB_ID,$DAVE_ID --trampolineFeesMsat=100000 --trampolineCltvExpiry=450 --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +Note that Alice didn't need to know about Carol. Bob will find the route to Dave through Carol on his own. That's the magic of trampoline! + +A couple gotchas: you need to make sure you specify the same `trampolineFeesMsat` and `trampolineCltvExpiry` as the first part. This is something we will improve if our users ask for a better API. + +You can then check the status of the payment with the `getsentinfo` command: + +```sh +eclair-cli getsentinfo --id=cd083b31-5939-46ac-bf90-8ac5b286a9e2 +``` + +Once Dave accepts the payment you should see all the details about the payment success (preimage, route, fees, etc). + \ No newline at end of file diff --git a/docs/Usage.md b/docs/Usage.md new file mode 100644 index 0000000000..ab73f483ae --- /dev/null +++ b/docs/Usage.md @@ -0,0 +1,90 @@ +# Usage + +## Command line with eclair-cli + +### Windows user + +On windows, you will have to install Bash first. We recommend installing Git Bash which is packed in the [Git installer from git-scm](https://git-scm.com/downloads). + +Then you'll have to install jq: + +- Get the latest windows version from https://stedolan.github.io/jq/download/ +- Rename the `jq-win64.exe` file to `jq.exe` and move it to `C:/Users/your_name/bin` + +## eclair-cli installation + +- Download the eclair-cli file from [our sources](https://github.com/ACINQ/eclair/blob/master/eclair-core/eclair-cli) +- (optional) Move the file to `~/bin` +- Enable the [JSON API](./API.md) in your `eclair.conf` settings. + +Run this command to list the available calls: + +```shell +./eclair-cli -p help +``` + +ℹ️ **Protip:** you can edit the `eclair-cli` file and save the API password/url so you don't have to set them every time. We will omit them in the examples below. + +Note that you may have to install jq first if it's not already installed on your machine: + +```shell +sudo apt-get install jq +``` + +## Example 1: open a channel with eclair-cli + +Your node listens on 8081. You want to open a 140 mBTC channel with `endurance.acinq.co` on Testnet. + +First connect to the endurance node: + +```shell +eclair-cli connect --uri=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@endurance.acinq.co:9735 +``` + +Then open a channel with endurance: + +```shell +eclair-cli open --nodeId=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134 --fundingSatoshis=14000000 +``` + +This will broadcast a funding transaction to the bitcoin blockchain. +Once that transaction is confirmed, your lightning channel will be ready for off-chain payments. + +:warning: You should NEVER use RBF to bump the fees of that funding transaction, otherwise you won't be able to recover your funds unless your peer cooperates. If the transaction doesn't confirm, you should use CPFP to bump the fees. + +## Example 2: pay an invoice with eclair-cli + +```shell +eclair-cli payinvoice --invoice=lntb17u1pdthhsdpp5z5am8....... +``` + +## Example 3: generate a payment request for 1 mBTC + +```shell +eclair-cli createinvoice --amountMsat=100000000 --description="my first invoice" | jq .serialized +``` + +This command will return a lightning payment request, such as: + +```shell +lntb1m1pdthhh0pp5063r8hu6f6hk7tpauhgvl3nnf4ur3xntcnhujcz5w82yq7nhjuysdq6d4ujqenfwfehggrfdemx76trv5xqrrss6uxhewtmjkumpr7w6prkgttku76azfq7l8cx9v74pcv85hzyvs9n23dhu9u354xcqpnzey45ua3g2m4dywuw7udrt2sdsvjf3rawdqcpas9mah +``` + +You can check this invoice with the `parseinvoice` command: + +```shell +eclair-cli parseinvoice --invoice=lntb1m1pdthhh0pp5063r8hu6f6hk7tpauhgvl3nnf4ur3xntcnhujcz5w82yq7nhjuysdq6d4ujqenfwfehggrfdemx76trv5xqrrss6uxhewtmjkumpr7w6prkgttku76azfq7l8cx9v74pcv85hzyvs9n23dhu9u354xcqpnzey45ua3g2m4dywuw7udrt2sdsvjf3rawdqcpas9mah +``` + +Which will breakdown the invoice in human readable data. + +## Example 4: list local channels in NORMAL state +```shell +eclair-cli channels | jq '.[] | select(.state == "NORMAL")' +``` + +## Example 5: compute your total balance across all channels +We divide by `1000` because we want satoshis, not millisatoshis. +```shell +eclair-cli usablebalances | jq 'map(.canSend / 1000) | add' +``` \ No newline at end of file diff --git a/docs/release-notes/eclair-v0.7.0.md b/docs/release-notes/eclair-v0.7.0.md new file mode 100644 index 0000000000..dc476c8e36 --- /dev/null +++ b/docs/release-notes/eclair-v0.7.0.md @@ -0,0 +1,344 @@ +# Eclair v0.7.0 + +This release adds official support for two long-awaited lightning features: anchor outputs and onion messages. +Support for the PostreSQL database backend is also now production ready! + +This release also includes a few bug fixes and many new (smaller) features. +It is fully compatible with 0.6.2 (and all previous versions of eclair). + +Because this release changes the default type of channels that your node will use, make sure you read the release notes carefully! + +## Major changes + +### Anchor outputs activated by default + +Experimental support for anchor outputs channels was first introduced in [eclair v0.4.2](https://github.com/ACINQ/eclair/releases/tag/v0.4.2). +Going from an experimental feature to production-ready required a lot more work than we expected! +What seems to be a simple change in transaction structure had a lot of impact in many unexpected places, and fundamentally changes the mechanisms to ensure funds safety. + +When using anchor outputs, you will need to keep utxos available in your `bitcoind` wallet to fee-bump HTLC transactions when channels are force-closed. +Eclair will warn you via the `notifications.log` file when your `bitcoind` wallet balance is too low to protect your funds against malicious channel peers. +Eclair will wait for you to add funds to your wallet and automatically retry, but you may be at risk in the meantime if you had pending payments at the time of the force-close. + +We recommend activating debug logs for the transaction publication mechanisms. +This will make it much easier to follow the trail of RBF-ed transactions, and shouldn't be too noisy unless you constantly have a lot of channels force-closing. +To activate these logs, simply add the following line to your `logback.xml`: + +```xml + +``` + +You don't even need to restart eclair, it will automatically pick up the logging configuration update after a short while. + +If you don't want to use anchor outputs, you can disable the feature in your `eclair.conf`: + +```conf +eclair.features.option_anchors_zero_fee_htlc_tx = disabled +``` + +### Postgres database backend graduates to production-ready + +Postgres support was introduced in Eclair 0.4.1 as beta, and has since been improved continuously over several versions. It is now production-ready and is the recommended database backend for larger nodes. + +Postgres offers superior administration capabilities, advanced JSON queries over channel data, streaming replication, and more, which makes it a great choice for enterprise setups. + +A [step-by-step migration guide](https://github.com/ACINQ/eclair/blob/master/docs/PostgreSQL.md#migrating-from-sqlite-to-postgres) from Sqlite to Postgres is provided for users who wish to do the switch. + +Sqlite remains fully supported. + +### Alternate strategy to avoid mass force-close of channels in certain cases + +The default strategy, when an unhandled exception or internal error happens, is to locally force-close the channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. + +This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node (with the default behavior, it would "only" cause a local force-close of the channel). + +### Separate log for important notifications + +Eclair added a new log file (`notifications.log`) for important notifications that require an action from the node operator. +Node operators should watch this file very regularly. + +An event is also sent to the event stream for every such notification. +This lets plugins notify the node operator via external systems (push notifications, email, etc). + +### Support for onion messages + +Eclair now supports the `option_onion_messages` feature (see . +This feature is enabled by default: eclair will automatically relay onion messages it receives. + +By default, eclair will only accept and relay onion messages from peers with whom you have channels. +You can change that strategy by updating `eclair.onion-messages.relay-policy` in your `eclair.conf`. + +Eclair applies some rate-limiting on the number of messages that can be relayed to and from each peer. +You can choose what limits to apply by updating `eclair.onion-messages.max-per-peer-per-second` in your `eclair.conf`. + +Whenever an onion message for your node is received, eclair will emit an event, that can for example be received on the websocket (`onion-message-received`). + +You can also send onion messages via the API. +This will be covered in the API changes section below. + +To disable the feature, you can simply update your `eclair.conf`: + +```conf +eclair.features.option_onion_messages = disabled +``` + +### Support for `option_payment_metadata` + +Eclair now supports the `option_payment_metadata` feature (see ). +This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid. +This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid. + +Eclair includes a small `payment_metadata` field in all invoices it generates. +This lets node operators verify that payers actually support that feature. + +### Optional safety checks when using Postgres + +When using postgres, at startup we optionally run a few basic safety checks, e.g. the number of local channels, how long since the last local channel update, etc. The goal is to make sure that we are connected to the correct database instance. + +Those checks are disabled by default because they wouldn't pass on a fresh new node with zero channels. You should enable them when you already have channels, so that there is something to compare to, and the values should be specific to your setup, particularly for local channels. Configuration is done by overriding `max-age` and `min-count` values in your `eclair.conf`: + +```conf +eclair.db.postgres.safety-checks + { + enabled = true + max-age { + local-channels = 3 minutes + network-nodes = 30 minutes + audit-relayed = 10 minutes + } + min-count { + local-channels = 10 + network-nodes = 3000 + network-channels = 20000 + } +} +``` + +### API changes + +#### Timestamps + +All timestamps are now returned as an object with two attributes: + +- `iso`: ISO-8601 format with GMT time zone. Precision may be second or millisecond depending on the timestamp. +- `unix`: seconds since epoch formats (seconds since epoch). Precision is always second. + +Examples: + +- second-precision timestamp: + - before: + + ```json + { + "timestamp": 1633357961 + } + ``` + + - after + + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41Z", + "unix": 1633357961 + } + } + ``` + +- milli-second precision timestamp: + - before: + + ```json + { + "timestamp": 1633357961456 + } + ``` + + - after (note how the unix format is in second precision): + + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41.456Z", + "unix": 1633357961 + } + } + ``` + +#### Sending onion messages + +You can now send onion messages with `sendonionmessage`. + +There are two ways to specify the recipient: + +- when you're sending to a known `nodeId`, you must set it in the `--recipientNode` field +- when you're sending to an unknown node behind a blinded route, you must provide the blinded route in the `--recipientBlindedRoute` field (hex-encoded) + +If you're not connected to the recipient and don't have channels with them, eclair will try connecting to them based on the best address it knows (usually from their `node_announcement`). +If that fails, or if you don't want to expose your `nodeId` by directly connecting to the recipient, you should find a route to them and specify the nodes in that route in the `--intermediateNodes` field. + +You can send arbitrary content to the recipient, by providing a hex-encoded tlv in the `--content` field. + +If you expect a response, you should provide a route from the recipient back to you in the `--replyPath` field. +Eclair will create a corresponding blinded route, and the API will wait for a response (or timeout if it doesn't receive a response). + +#### Balance + +The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. + +Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are +now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. + +#### Miscellaneous + +This release contains many other API updates: + +- `deleteinvoice` allows you to remove unpaid invoices (#1984) +- `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) +- `getsentinfo` output includes `failedNode` field for all failed routes +- for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` +- for `open`, `--channelFlags` has been replaced by `--announceChannel` +- the `networkstats` API has been removed + +Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. + +### Miscellaneous improvements and bug fixes + +- Eclair now supports cookie authentication for Bitcoin Core RPC (#1986) +- Eclair echoes back the remote peer's IP address in `init` (#1973) +- Eclair now supports [circular rebalancing](https://github.com/ACINQ/eclair/blob/master/docs/CircularRebalancing.md) + +## Verifying signatures + +You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: + +- from our website: https://acinq.co/pgp/drouinf.asc +- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys + +To import our signing key: + +```sh +$ gpg --import drouinf.asc +``` + +To verify the release file checksums and signatures: + +```sh +$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped +$ sha256sum -c SHA256SUMS.stripped +``` + +## Building + +Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): + +- Ubuntu 20.04 +- AdoptOpenJDK 11.0.6 +- Maven 3.8.1 + +Use the following command to generate the eclair-node package: + +```sh +mvn clean install -DskipTests +``` + +That should generate `eclair-node/target/eclair-node-0.7.0-XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` + +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. + +## Upgrading + +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. + +## Changelog + +- [57bf860](https://github.com/ACINQ/eclair/commit/57bf86044ec4e2ff09d9639e986d04f88fde2305) Back to Dev (#1993) +- [df63ea4](https://github.com/ACINQ/eclair/commit/df63ea47835af6c61dbf37933707752ed06812c1) Deprecation warning for relay fees config (#2012) +- [498e9a7](https://github.com/ACINQ/eclair/commit/498e9a7db1deb5274380ee8dd47e78fef95a854f) Remove CoinUtils.scala. (#2013) +- [b22b1cb](https://github.com/ACINQ/eclair/commit/b22b1cbea738102a7c63fdbf1366254ec0dffbfe) Fix API hanging on invalid remote params (#2008) +- [9057c8e](https://github.com/ACINQ/eclair/commit/9057c8e90a9b8301b575fd90ac50f123fc0d203f) Minor improvements (#1998) +- [b4d285f](https://github.com/ACINQ/eclair/commit/b4d285f1c4c2cccfcd8b052e451a3b428e8996b0) Proper types for UNIX timestamps (#1990) +- [6018988](https://github.com/ACINQ/eclair/commit/601898864d18316ba759fa8844a2d846c729819d) Check serialization consistency in all channel tests (#1994) +- [6b202c3](https://github.com/ACINQ/eclair/commit/6b202c392bc2fad31bc33affc40577dc7577ed3d) Add low-level route blinding features (#1962) +- [f3b1604](https://github.com/ACINQ/eclair/commit/f3b16047eb188395454ae1479c4fd62f12ca9cdb) Add API to delete an invoice (#1984) +- [93481d9](https://github.com/ACINQ/eclair/commit/93481d99433b35278fe1746dc089df58f8935a9a) Higher `walletpassphrase` timeout in tests (#2022) +- [bdef833](https://github.com/ACINQ/eclair/commit/bdef8337e89bf5f31e7cb76e700c6b51fadd673f) Additional parameters for findroute* API calls (#1969) +- [9274582](https://github.com/ACINQ/eclair/commit/9274582679f0b7cbce675182019ce82c75a85305) Balance: take signed fulfills into account (#2023) +- [4e9190a](https://github.com/ACINQ/eclair/commit/4e9190aaee9207ce83152fc89e90e2c5c041a7a1) Minor: higher timeout in payment fsm test (#2026) +- [28d04ba](https://github.com/ACINQ/eclair/commit/28d04ba7a79b6fa3a20a9f3afeee43a639fc74ea) Store blinding pubkey for introduction node (#2024) +- [570dc22](https://github.com/ACINQ/eclair/commit/570dc223da6a3a92f7cf90efe6d3299800f990c3) Fix flaky transaction published event test (#2020) +- [99a8896](https://github.com/ACINQ/eclair/commit/99a889636b0dd3dc5c587a82bc032e6089e9a30c) ignoreShortChannelIds should disable edges in both directions (#2032) +- [494e346](https://github.com/ACINQ/eclair/commit/494e346231c9016d4ee71b92b99b76d891b3aeed) Minor: put htlc info logs in debug (#2030) +- [765a0c5](https://github.com/ACINQ/eclair/commit/765a0c5436a8e2ec1f469f1b066910f1205c4c99) Add log file for important notifications (#1982) +- [1573f7b](https://github.com/ACINQ/eclair/commit/1573f7be05931bb722c085eda7577c1e9731f04e) EncryptedRecipientData TLV stream should not be length-prefixed (#2029) +- [e54aaa8](https://github.com/ACINQ/eclair/commit/e54aaa84bef071c189f3fcfd7fb0e687a0d0dcbb) API: fix default time boundaries (#2035) +- [c5fa39f](https://github.com/ACINQ/eclair/commit/c5fa39f75480e859c39bd3b5708123152a067c98) Front: stop the jvm after coordinated shutdown (#2028) +- [2e9f8d9](https://github.com/ACINQ/eclair/commit/2e9f8d9f9eea47df251698852b7eb695f271f078) Cookie-based authentication for Bitcoin Core RPC (#1986) +- [2c0c24e](https://github.com/ACINQ/eclair/commit/2c0c24e1e1565a33fee1e0a3adf69a5d0be582f2) Rework channel reestablish (#2036) +- [4f458d3](https://github.com/ACINQ/eclair/commit/4f458d356ce039b1d9b6ab361b71e07427c96ba2) Alternate strategy for unhandled exceptions (#2037) +- [9f65f3a](https://github.com/ACINQ/eclair/commit/9f65f3a3a9ebedfcc7186ad0422a58d477d1b090) Make compatibility code for `waitingSince` work on testnet (#2041) +- [1f613ec](https://github.com/ACINQ/eclair/commit/1f613ec7a328de6a4f6551aaa3ad31ec3c1cb8a6) Handle mutual close published from the outside (#2046) +- [f7a79d1](https://github.com/ACINQ/eclair/commit/f7a79d10b4ddb6f91a4c66983c7a1620cc572e72) Fix response for `updaterelayfee` (#2047) +- [3dc4ae1](https://github.com/ACINQ/eclair/commit/3dc4ae1099fa1a2a3bb3742955f3c61a5c6704bc) Refactor payment onion utilities (#2051) +- [b45dd00](https://github.com/ACINQ/eclair/commit/b45dd0078ecf56d88098a74e5f2887a20c8aa4c1) Refactor sphinx payment packet (#2052) +- [4ac8236](https://github.com/ACINQ/eclair/commit/4ac823620b5bc734b365aa7edce572c0e79d00a0) Remove dumpprivkey from tests (#2053) +- [083dc3c](https://github.com/ACINQ/eclair/commit/083dc3c8da0c78414e63ae001aba126cdef80662) Onion messages (#1957) +- [333e9ef](https://github.com/ACINQ/eclair/commit/333e9ef04f0c74ca54bcf41360f895e675a3b28e) Clarify route blinding types (#2059) +- [6cc37cb](https://github.com/ACINQ/eclair/commit/6cc37cbd4f8192162e916ddabe3a612fafed06a2) Simplify onion message codec (#2060) +- [fb96e5e](https://github.com/ACINQ/eclair/commit/fb96e5eb3f29bce372af1dd392e169fd1bb8b3f1) Add failed node ID field to `FailureSummary` (#2042) +- [59b4035](https://github.com/ACINQ/eclair/commit/59b403559bdfc0ba884503bb12a14846b7aa5d2d) Relay onion messages (#2061) +- [9792c72](https://github.com/ACINQ/eclair/commit/9792c725c777d18d538e16d55ac89a847ba8beef) Rename feeThresholdSat to maxFeeFlatSat (#2079) +- [4ad502c](https://github.com/ACINQ/eclair/commit/4ad502c4c897af8f37a62bb0dfc9504ff5c9c0fc) Abort HTLC tx publisher if remote commit confirms (#2080) +- [589e84f](https://github.com/ACINQ/eclair/commit/589e84feabcb623898d2cfa62ecac70453a24645) Support private channels in SendToRoute (#2082) +- [86aed63](https://github.com/ACINQ/eclair/commit/86aed633e9741f4f604edf0d9aa507bbd12b0cfe) Remove misleading claim-htlc-success log (#2076) +- [bacb31c](https://github.com/ACINQ/eclair/commit/bacb31cf8d6db9ea323f0366a859686b632411ca) Add channel type feature bit (#2073) +- [3003289](https://github.com/ACINQ/eclair/commit/3003289328c091527f248b118e9280657417481d) No error when updating the relay fees not in normal state (#2086) +- [a470d41](https://github.com/ACINQ/eclair/commit/a470d41a9592e34d8b3b27e209f4991ac0247a34) Write received onion messages to the websocket (#2091) +- [62cc073](https://github.com/ACINQ/eclair/commit/62cc073d670e9e7407e2a8c5819292f7a829346f) Remove network stats computation (#2094) +- [ee852d6](https://github.com/ACINQ/eclair/commit/ee852d6320b567f5c5e463bdb2c2e69f1ae011c8) (Minor) Handle disconnect request when offline (#2095) +- [40cc458](https://github.com/ACINQ/eclair/commit/40cc4580438691434c5bb22a87c79b23cbc8502a) Switchboard exposes peer information (#2097) +- [3451974](https://github.com/ACINQ/eclair/commit/34519749d240e2a00b9cdc1e9e9933feeaa9a51a) Raise default connection timeout values (#2093) +- [ac9e274](https://github.com/ACINQ/eclair/commit/ac9e274f0d1083319f70e08d495f20e27dd4cccd) Add message relay policies (#2099) +- [535daec](https://github.com/ACINQ/eclair/commit/535daec0657acf2e05f5c8fca046ae9ab3f70434) Fix unhandled event in DbEventHandler (#2103) +- [4ebc8b5](https://github.com/ACINQ/eclair/commit/4ebc8b5d6b64746b8828a81765d7860c0dda5c80) Notify node operator on low fee bumping reserve (#2104) +- [576c0f6](https://github.com/ACINQ/eclair/commit/576c0f6e39783755ada0efe079e2ec3164a01842) Increase timeout onion message integration tests (#2106) +- [3d88c43](https://github.com/ACINQ/eclair/commit/3d88c43b12ab1e0c4fd4030f408119df4e4c0136) Kill idle peers (#2096) +- [8ff7dc7](https://github.com/ACINQ/eclair/commit/8ff7dc713a5024397fd687dbd2c9ccdae6aac567) Avoid default arguments in test helpers (#2108) +- [7e7de53](https://github.com/ACINQ/eclair/commit/7e7de53d1de4cba79280f08f3d4d6cf227ba42d2) Filter out non-standard channels from `channelInfo` API (#2107) +- [ec13281](https://github.com/ACINQ/eclair/commit/ec132810a574a2832933b9b2432fb50a7abd41b7) Add nightly build with bitcoind master (#2027) +- [c370abe](https://github.com/ACINQ/eclair/commit/c370abe5c3c0333c1e650383da736c5f0721b7be) Rate limit onion messages (#2090) +- [8afb02a](https://github.com/ACINQ/eclair/commit/8afb02ab97b216e7d9fd67454830b84d19115fc6) Add payment hash to `ClaimHtlcSuccessTx` (#2101) +- [bf0969a](https://github.com/ACINQ/eclair/commit/bf0969a3081800713b488e432c5fd99b5506c8a6) Add defenses against looping paths (#2109) +- [7693696](https://github.com/ACINQ/eclair/commit/76936961f9cd6c794bab90879269372f5723c7ae) Fix potential loop in path-finding (#2111) +- [0b807d2](https://github.com/ACINQ/eclair/commit/0b807d257a1ee1d33830fb91a11aa47ba7d3dcb0) Set max timestamp in API call to 9999/12/31 (#2112) +- [2827be8](https://github.com/ACINQ/eclair/commit/2827be875d2ebb5f941c7e2a876a214469d698be) Make `ClaimHtlcTx` replaceable (#2102) +- [1fd6344](https://github.com/ACINQ/eclair/commit/1fd6344a5d2a89bf7993193678520f2dc27a60bd) Define 9999-12-31 as max value for timestamps (#2118) +- [fda3818](https://github.com/ACINQ/eclair/commit/fda3818c6a38688dbc19a8bd7f0e10632876075c) Rename raw tx publisher to final tx publisher (#2119) +- [7421098](https://github.com/ACINQ/eclair/commit/7421098c4455efb2548d620ce2197bab610a2a69) Process replies to onion messages (#2117) +- [27579a5](https://github.com/ACINQ/eclair/commit/27579a5786db083700bb668a93eb86583bdf9d74) (Minor) Use `sys` package instead of `System` when applicable (#2124) +- [6e88532](https://github.com/ACINQ/eclair/commit/6e88532d18874f04b964029ce00b4008cbee3102) Add support for `option_payment_metadata` (#2063) +- [7f7ee03](https://github.com/ACINQ/eclair/commit/7f7ee0389926279b2d3277f2914f61e19c99a1f2) Handle onion creation failures (#2087) +- [546ca23](https://github.com/ACINQ/eclair/commit/546ca23984d3010e22b7bb1695220a9eed3cb00d) Activate onion messages (#2133) +- [27e29ec](https://github.com/ACINQ/eclair/commit/27e29ecebf42da223e53bfd9f1b4da3dd65953f3) Fix onion message tests by changing the rate limiter (#2132) +- [88dffbc](https://github.com/ACINQ/eclair/commit/88dffbc28c48ebbcf977333a84f9af7ff128b39a) Publish docker images (#2130) +- [40f7ff4](https://github.com/ACINQ/eclair/commit/40f7ff4034f3df32f5c5dc4f6a6af30d408cfcc5) Replaceable txs fee bumping (#2113) +- [58f9ebc](https://github.com/ACINQ/eclair/commit/58f9ebc6243563bee187478ab8d35e4315eb856d) Use BlockHeight everywhere (#2129) +- [52a6ee9](https://github.com/ACINQ/eclair/commit/52a6ee905969b66b0d912129bd29e3b58acea43d) Rename TxPublishLogContext (#2131) +- [e2b1b26](https://github.com/ACINQ/eclair/commit/e2b1b26919ed886b0e178c55a001585a38abfb16) Activate anchor outputs (#2134) +- [2f07b3e](https://github.com/ACINQ/eclair/commit/2f07b3ec0b8934bede851af93dcdb6145be585fb) (Minor) Nits (#2139) +- [f8d507b](https://github.com/ACINQ/eclair/commit/f8d507bbdd935ef5ca4eb8ce280300fdb3c1996a) Send remote address in init (#1973) +- [c180ca2](https://github.com/ACINQ/eclair/commit/c180ca2ef10531307d3441aad5b83f139eeb1c40) Postgres: add safety checks at startup (#2140) +- [0a37213](https://github.com/ACINQ/eclair/commit/0a37213483e75fbf1974ce4f6ec830596a79ae2b) Add randomization on node addresses (#2123) +- [85f9455](https://github.com/ACINQ/eclair/commit/85f94554c0c513011cabd0149d581f3d7505686f) Fix error logs formatting in TxPublisher (#2136) +- [2d64187](https://github.com/ACINQ/eclair/commit/2d641870731075f9c4ee5eef2e84f9368f410464) More aggressive confirmation target when low on utxos (#2141) +- [d59d434](https://github.com/ACINQ/eclair/commit/d59d4343c82cdd6fecddec5e44ec2cd95dbd1571) Improve `getsentinfo` accuracy (#2142) +- [f3604cf](https://github.com/ACINQ/eclair/commit/f3604cffaf9df9c8fffd5142f5dc9387fcfc74b9) Document onchain wallet backup. (#2143) +- [8758d50](https://github.com/ACINQ/eclair/commit/8758d50df26ba2e7007f5ab4243eaf556fd32d35) Update cluster documentation [ci skip] (#2122) +- [75ef66e](https://github.com/ACINQ/eclair/commit/75ef66e54c387ea7833465d8d34e8f6627bcbff2) Front: use IMDSv2 to retrieve the instance ip (#2146) +- [57c2cc5](https://github.com/ACINQ/eclair/commit/57c2cc5df907f2d848daf3d40cf2ceb6ff603ede) Type `ChannelFlags` instead of using a raw `Byte` (#2148) +- [ffecd62](https://github.com/ACINQ/eclair/commit/ffecd62cc133bb64aa916ed8d066b8ab6d7597ec) Move channel parameters to their own conf section (#2149) +- [1f6a7af](https://github.com/ACINQ/eclair/commit/1f6a7afd47e989b7c083ae9da075a9e1dd88e457) Have sqlite also write to jdbc url file (#2153) +- [0333e11](https://github.com/ACINQ/eclair/commit/0333e11c79de838cfd2bd7946d0c29b2bae3a8d6) Database migration Sqlite->Postgres (#2156) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index d5eec51a97..36e9456b3e 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,185 +4,144 @@ ## Major changes -### Anchor outputs activated by default +### Add support for channel aliases and zeroconf channels -Experimental support for anchor outputs channels was first introduced in [eclair v0.4.2](https://github.com/ACINQ/eclair/releases/tag/v0.4.2). -Going from an experimental feature to production-ready required a lot more work than we expected! -What seems to be a simple change in transaction structure had a lot of impact in many unexpected places, and fundamentally changes the mechanisms to ensure funds safety. +:information_source: Those features are only supported for channels of type `AnchorOutputsZeroFeeHtlcTx`, which is the +newest channel type and the one enabled by default. If you are opening a channel with a node that doesn't run Eclair, +make sure they support `option_anchors_zero_fee_htlc_tx`. -When using anchor outputs, you will need to keep utxos available in your `bitcoind` wallet to fee-bump HTLC transactions when channels are force-closed. -Eclair will warn you via the `notifications.log` file when your `bitcoind` wallet balance is too low to protect your funds against malicious channel peers. -Eclair will wait for you to add funds to your wallet and automatically retry, but you may be at risk in the meantime if you had pending payments at the time of the force-close. +#### Channel aliases -We recommend activating debug logs for the transaction publication mechanisms. -This will make it much easier to follow the trail of RBF-ed transactions, and shouldn't be too noisy unless you constantly have a lot of channels force-closing. -To activate these logs, simply add the following line to your `logback.xml`: +Channel aliases offer a way to use arbitrary channel identifiers for routing. This feature improves privacy by not +leaking the funding transaction of the channel during payments. -```xml - -``` - -You don't even need to restart eclair, it will automatically pick up the logging configuration update after a short while. +This feature is enabled by default, but your peer has to support it too, and it is not compatible with public channels. -If you don't want to use anchor outputs, you can disable the feature in your `eclair.conf`: +#### Zeroconf channels -```conf -eclair.features.option_anchors_zero_fee_htlc_tx = disabled -``` +Zeroconf channels make it possible to use a newly created channel before the funding tx is confirmed on the blockchain. -### Alternate strategy to avoid mass force-close of channels in certain cases +:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you can +only enable it on a peer-by-peer basis. -The default strategy, when an unhandled exception or internal error happens, is to locally force-close the channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. +##### Enabling through features -This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node (with the default behavior, it would "only" cause a local force-close of the channel). +Below is how to enable zeroconf with a given peer in `eclair.conf`. With this config, your node will _accept_ zeroconf +channels from node `03864e...`. -### Separate log for important notifications +```eclair.conf +override-init-features = [ + { + nodeid = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + features = { + // dependencies of zeroconf + option_static_remotekey = optional + option_anchors_zero_fee_htlc_tx = optional + option_scid_alias = optional + // enable zeroconf + option_zeroconf = optional + } + } +] +``` -Eclair added a new log file (`notifications.log`) for important notifications that require an action from the node operator. -Node operators should watch this file very regularly. +Note that, as funder, Eclair will happily use an unconfirmed channel if the peer sends an early `channel_ready`, even if +the `option_zeroconf` feature isn't enabled, as long as the peer provides a channel alias. -An event is also sent to the event stream for every such notification. -This lets plugins notify the node operator via external systems (push notifications, email, etc). +##### Enabling through channel type -### Support for onion messages +You can enable `option_scid_alias` and `option_zeroconf` features by requesting them in the channel type, even if those +options aren't enabled in your features. -Eclair now supports the `option_onion_messages` feature (see . -This feature is enabled by default: eclair will automatically relay onion messages it receives. +The new channel types variations are: -By default, eclair will only accept and relay onion messages from peers with whom you have channels. -You can change that strategy by updating `eclair.onion-messages.relay-policy` in your `eclair.conf`. +- `anchor_outputs_zero_fee_htlc_tx+scid_alias` +- `anchor_outputs_zero_fee_htlc_tx+zeroconf` +- `anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf` -Eclair applies some rate-limiting on the number of messages that can be relayed to and from each peer. -You can choose what limits to apply by updating `eclair.onion-messages.max-per-peer-per-second` in your `eclair.conf`. +Examples using the command-line interface: -Whenever an onion message for your node is received, eclair will emit an event, that can for example be received on the websocket (`onion-message-received`). +- open a public zeroconf channel: -You can also send onion messages via the API. -This will be covered in the API changes section below. +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+zeroconf --announceChannel=true +``` -To disable the feature, you can simply update your `eclair.conf`: +- open a private zeroconf channel with aliases: -```conf -eclair.features.option_onion_messages = disabled +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false ``` -### Support for `option_payment_metadata` +### Changes to features override -Eclair now supports the `option_payment_metadata` feature (see https://github.com/lightning/bolts/pull/912). -This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid. -This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid. +Eclair supports overriding features on a per-peer basis, using the `eclair.override-init-features` field in `eclair.conf`. +These overrides will now be applied on top of the default features, whereas the previous behavior was to completely ignore default features. +We provide detailed examples in the [documentation](../Configure.md#customize-features). -Eclair includes a small `payment_metadata` field in all invoices it generates. -This lets node operators verify that payers actually support that feature. +### Remove support for Tor v2 -### Optional safety checks when using Postgres +Dropped support for version 2 of Tor protocol. That means: -When using postgres, at startup we optionally run a few basic safety checks, e.g. the number of local channels, how long since the last local channel update, etc. The goal is to make sure that we are connected to the correct database instance. +- Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore +- Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon -Those checks are disabled by default because they wouldn't pass on a fresh new node with zero channels. You should enable them when you already have channels, so that there is something to compare to, and the values should be specific to your setup, particularly for local channels. Configuration is done by overriding `max-age` and `min-count` values in your `eclair.conf`: -``` -eclair.db.postgres.safety-checks - { - enabled = true - max-age { - local-channels = 3 minutes - network-nodes = 30 minutes - audit-relayed = 10 minutes - } - min-count { - local-channels = 10 - network-nodes = 3000 - network-channels = 20000 - } -} -``` +IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before +upgrading to this release. ### API changes -#### Timestamps - -All timestamps are now returned as an object with two attributes: - -- `iso`: ISO-8601 format with GMT time zone. Precision may be second or millisecond depending on the timestamp. -- `unix`: seconds since epoch formats (seconds since epoch). Precision is always second. +- `channelbalances`: retrieves information about the balances of all local channels (#2196) +- `stop`: stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323) -Examples: - -- second-precision timestamp: - - before: - ```json - { - "timestamp": 1633357961 - } - ``` - - after - ```json - { - "timestamp": { - "iso": "2021-10-04T14:32:41Z", - "unix": 1633357961 - } - } - ``` -- milli-second precision timestamp: - - before: - ```json - { - "timestamp": 1633357961456 - } - ``` - - after (note how the unix format is in second precision): - ```json - { - "timestamp": { - "iso": "2021-10-04T14:32:41.456Z", - "unix": 1633357961 - } - } - ``` - -#### Sending onion messages +### Miscellaneous improvements and bug fixes -You can now send onion messages with `sendonionmessage`. +#### Delay enforcement of new channel fees -There are two ways to specify the recipient: +When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even +if they would be rejected with the new fee. +By default, eclair will still accept the old fee for 10 minutes, you can change it by +setting `eclair.relay.fees.enforcement-delay` to a different value. -- when you're sending to a known `nodeId`, you must set it in the `--recipientNode` field -- when you're sending to an unknown node behind a blinded route, you must provide the blinded route in the `--recipientBlindedRoute` field (hex-encoded) +If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the +previous fee. -If you're not connected to the recipient and don't have channels with them, eclair will try connecting to them based on the best address it knows (usually from their `node_announcement`). -If that fails, or if you don't want to expose your `nodeId` by directly connecting to the recipient, you should find a route to them and specify the nodes in that route in the `--intermediateNodes` field. +#### New minimum funding setting for private channels -You can send arbitrary content to the recipient, by providing a hex-encoded tlv in the `--content` field. +New settings have been added to independently control the minimum funding required to open public and private channels +to your node. -If you expect a response, you should provide a route from the recipient back to you in the `--replyPath` field. -Eclair will create a corresponding blinded route, and the API will wait for a response (or timeout if it doesn't receive a response). +The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings +and defaults: -#### Balance +- `eclair.channel.min-public-funding-satoshis = 100000` +- `eclair.channel.min-private-funding-satoshis = 100000` -The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. +If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these +new settings. -Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are -now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. +#### Expired incoming invoices now purged if unpaid -#### Miscellaneous +Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. +Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or +change the search interval with two new settings: -This release contains many other API updates: +- `eclair.purge-expired-invoices.enabled = true +- `eclair.purge-expired-invoices.interval = 24 hours` -- `deleteinvoice` allows you to remove unpaid invoices (#1984) -- `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) -- `getsentinfo` output includes `failedNode` field for all failed routes -- for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` -- the `networkstats` API has been removed +#### Skip anchor CPFP for empty commitment -Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. +When using anchor outputs and a channel force-closes without HTLCs in the commitment transaction, funds cannot be stolen by your counterparty. +In that case eclair can skip spending the anchor output to save on-chain fees, even if the transaction doesn't confirm. +This can be activated by setting the following value in your `eclair.conf`: -### Miscellaneous improvements and bug fixes +```conf +eclair.on-chain-fees.spend-anchor-without-htlcs = false +``` -- Eclair now supports cookie authentication for Bitcoin Core RPC (#1986) +This is disabled by default, because there is still a risk of losing funds until bitcoin adds support for package relay. +If the mempool becomes congested and the feerate is too low, the commitment transaction may never reach miners' mempools because it's below the minimum relay feerate. ## Verifying signatures @@ -218,14 +177,17 @@ Use the following command to generate the eclair-node package: mvn clean install -DskipTests ``` -That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one +we provide and sign in `SHA256SUMS.asc` -(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not +tried everything. ## Upgrading -This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, +upgrade and restart. ## Changelog - + diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 4b6e320164..22be3fe5f5 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -34,6 +34,7 @@ and COMMAND is one of the available commands: - disconnect - peers - audit + - stop === Channel === - open @@ -44,6 +45,7 @@ and COMMAND is one of the available commands: - allchannels - allupdates - channelstats + - channelbalances === Fees === - networkfees diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 80603e99c3..a511f4a6f2 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.1-SNAPSHOT eclair-core_2.13 @@ -160,18 +160,24 @@ - com.softwaremill.sttp + com.softwaremill.sttp.client3 okhttp-backend_${scala.version.short} ${sttp.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + org.json4s json4s-jackson_${scala.version.short} - 3.6.11 + 4.0.3 - com.softwaremill.sttp + com.softwaremill.sttp.client3 json4s_${scala.version.short} ${sttp.version} @@ -202,7 +208,13 @@ org.scodec scodec-core_${scala.version.short} - 1.11.8 + 1.11.9 + + + + org.scodec + scodec-bits_${scala.version.short} + 1.1.30 commons-codec @@ -228,8 +240,8 @@ org.postgresql - 42.2.23 postgresql + 42.3.3 com.zaxxer @@ -258,6 +270,11 @@ 1.5.0 + + io.kamon + kamon-prometheus_${scala.version.short} + 2.5.4 + io.kamon kamon-core_${scala.version.short} @@ -290,7 +307,7 @@ org.mockito mockito-scala-scalatest_${scala.version.short} - 1.5.9 + 1.17.5 test diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 2e382f743e..f612c769a8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -16,8 +16,6 @@ eclair { password = "" // password for basic auth, must be non empty if json-rpc api is enabled } - watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s - bitcoind { host = "localhost" rpcport = 8332 @@ -31,6 +29,10 @@ eclair { wallet = "" zmqblock = "tcp://127.0.0.1:29000" zmqtx = "tcp://127.0.0.1:29000" + // Batching requests saves bandwidth but may slightly degrade latency and reliability. It is useful for the watcher, + // which can generate a lot of requests to validate channels, iterate over blocks to find a spending tx, etc. + // You may want to disable this when bitcoin is running on a remote machine with an unreliable network. + batch-watcher-requests = true } node-alias = "eclair" @@ -56,58 +58,76 @@ eclair { option_anchor_outputs = disabled option_anchors_zero_fee_htlc_tx = optional option_shutdown_anysegwit = optional + option_dual_fund = disabled option_onion_messages = optional option_channel_type = optional + option_scid_alias = optional option_payment_metadata = optional - trampoline_payment = disabled + // By enabling option_zeroconf, you will be trusting your peer as fundee. You will lose funds if they double spend + // their funding tx. Eclair does not let you activate this feature by default, you have to activate it for every + // node that you trust using override-init-features (see below). + option_zeroconf = disabled keysend = disabled + trampoline_payment_prototype = disabled } - override-features = [ // optional per-node features + // The following section lets you customize features for specific nodes. + // The overrides will be applied on top of the default features settings. + override-init-features = [ // optional per-node features # { - # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # features { } # } ] sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers - channel-flags = 1 // announce channels - dust-limit-satoshis = 546 - max-remote-dust-limit-satoshis = 600 - htlc-minimum-msat = 1 - // The following parameters apply to each HTLC direction (incoming or outgoing), which means that the total HTLC limits will be twice what is set here - max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC - max-accepted-htlcs = 30 + channel { + channel-flags { + announce-channel = true + } - reserve-to-funding-ratio = 0.01 // recommended by BOLT #2 - max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%) + dust-limit-satoshis = 546 + max-remote-dust-limit-satoshis = 600 + htlc-minimum-msat = 1 + // The following parameters apply to each HTLC direction (incoming or outgoing), which means that the total HTLC limits will be twice what is set here + max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC + max-accepted-htlcs = 30 + + reserve-to-funding-ratio = 0.01 // recommended by BOLT #2 + max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%) + min-public-funding-satoshis = 100000 + min-private-funding-satoshis = 100000 + max-funding-satoshis = 16777215 // to open channels larger than 16777215 you must enable the large_channel_support feature in 'eclair.features' + + to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days) + max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks) + mindepth-blocks = 3 + expiry-delta-blocks = 144 + // When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to + // avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between + // our HTLC-success and their HTLC-timeout). + // We will close the channel when the HTLC-timeout will happen in less than this number. + // NB: this number effectively reduces the expiry-delta-blocks, so you may want to take that into account and increase + // expiry-delta-blocks. + fulfill-safety-before-timeout-blocks = 24 + min-final-expiry-delta-blocks = 30 // Bolt 11 invoice's min_final_cltv_expiry; must be strictly greater than fulfill-safety-before-timeout-blocks + max-block-processing-delay = 30 seconds // we add a random delay before processing blocks, capped at this value, to prevent herd effect + max-tx-publish-retry-delay = 60 seconds // we add a random delay before retrying failed transaction publication + + // The default strategy, when we encounter an unhandled exception or internal error, is to locally force-close the + // channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some + // misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. + // This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely + // monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a + // revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the + // outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node + // (with the default behavior, it would "only" cause a local force-close of the channel). + unhandled-exception-strategy = "local-close" // local-close or stop + + revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting + } balance-check-interval = 1 hour - to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days) - max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks) - mindepth-blocks = 3 - expiry-delta-blocks = 144 - // When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to - // avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between - // our HTLC-success and their HTLC-timeout). - // We will close the channel when the HTLC-timeout will happen in less than this number. - // NB: this number effectively reduces the expiry-delta-blocks, so you may want to take that into account and increase - // expiry-delta-blocks. - fulfill-safety-before-timeout-blocks = 24 - min-final-expiry-delta-blocks = 30 // Bolt 11 invoice's min_final_cltv_expiry; must be strictly greater than fulfill-safety-before-timeout-blocks - max-block-processing-delay = 30 seconds // we add a random delay before processing blocks, capped at this value, to prevent herd effect - max-tx-publish-retry-delay = 60 seconds // we add a random delay before retrying failed transaction publication - - // The default strategy, when we encounter an unhandled exception or internal error, is to locally force-close the - // channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some - // misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. - // This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely - // monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a - // revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the - // outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node - // (with the default behavior, it would "only" cause a local force-close of the channel). - unhandled-exception-strategy = "local-close" // local-close or stop - relay { fees { // Fees for public channels @@ -125,6 +145,8 @@ eclair { fee-base-msat = 1000 fee-proportional-millionths = 100 } + // Delay enforcement of channel fee updates + enforcement-delay = 10 minutes } } @@ -145,12 +167,18 @@ eclair { // number of blocks to target when computing fees for each transaction type target-blocks { - funding = 6 // target for the funding transaction - commitment = 2 // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing* - commitment-without-htlcs = 12 // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing* - mutual-close = 12 // target for the mutual close transaction - claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet) - safe-utxos-threshold = 10 // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios + // target for the funding transaction + funding = 6 + // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing* + commitment = 2 + // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing* + commitment-without-htlcs = 12 + // target for the mutual close transaction + mutual-close = 12 + // target for the claim main transaction (tx that spends main channel output back to wallet) + claim-main = 12 + // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios + safe-utxos-threshold = 10 } feerate-tolerance { @@ -185,43 +213,48 @@ eclair { # } ] + // if false, the commitment transaction will not be fee-bumped when we have no htlcs to claim (used in force-close scenario) + // *do not change this unless you know what you are doing* + spend-anchor-without-htlcs = true + close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing - // funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater - // than this ratio. + // the channel initiator will send an UpdateFee message if the difference between current commitment fee and actual + // current network fee is greater than this ratio. update-fee-min-diff-ratio = 0.1 } - revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting - peer-connection { auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe ping-interval = 30 seconds ping-timeout = 20 seconds // will disconnect if peer takes longer than that to respond ping-disconnect = true // disconnect if no answer to our pings + // When enabled, if we receive an incoming connection, we will echo the source IP address in our init message. + // This should be disabled if your node is behind a load balancer that doesn't preserve source IP addresses. + send-remote-address-init = true } auto-reconnect = true initial-random-reconnect-delay = 5 seconds // we add a random delay before the first reconnection attempt, capped by this value max-reconnect-interval = 1 hour // max interval between two reconnection attempts, after the exponential backoff period - payment-request-expiry = 1 hour // default expiry for payment requests generated by this node + invoice-expiry = 1 hour // default expiry for invoices generated by this node multi-part-payment-expiry = 60 seconds // default expiry for receiving all parts of a multi-part payment - min-funding-satoshis = 100000 - max-funding-satoshis = 16777215 // to open channels larger than 16777215 you must enable the large_channel_support feature in 'eclair.features' max-payment-attempts = 5 autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels router { + watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s + channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 init-timeout = 5 minutes + balance-estimate-half-life = 1 day // time after which the confidence of the balance estimate is halved sync { request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know - encoding-type = zlib // encoding for short_channel_ids and timestamps in query channel sync messages; other possible value is "uncompressed" channel-range-chunk-size = 1500 // max number of short_channel_ids (+ timestamps + checksums) in reply_channel_range *do not change this unless you know what you are doing* channel-query-chunk-size = 100 // max number of short_channel_ids in query_short_channel_ids *do not change this unless you know what you are doing* } @@ -247,18 +280,27 @@ eclair { channel-age = 0.4 // when computing the weight for a channel, consider its AGE in this proportion channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion } + + hop-cost { + // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path + fee-base-msat = 500 + fee-proportional-millionths = 200 + } + locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked. // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. + // virtual fee for failed payments: how much you are willing to pay to get one less failed payment attempt + // ignored if use-ratio = true failure-cost { fee-base-msat = 2000 fee-proportional-millionths = 500 } - hop-cost { - // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path - fee-base-msat = 500 - fee-proportional-millionths = 200 - } + // Using a failure cost breaks Dijkstra (the path returned is no longer guaranteed to be shortest one), if + // that's a concern, you can penalize paths with a low success chance by using the logarithm of the probability + // of success. It satisfies Dijkstra's requirements and is a very good approximation for paths with a high + // probability of success, however is penalizes less the paths with a low probability of success. + use-log-probability = false mpp { min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs @@ -295,7 +337,6 @@ eclair { tor { enabled = false - protocol = "v3" // v2, v3 auth = "password" // safecookie, password password = "foobar" // used when auth=password host = "127.0.0.1" @@ -306,7 +347,7 @@ eclair { } db { - driver = "sqlite" // sqlite, postgres + driver = "sqlite" // sqlite, postgres, dual-sqlite-primary, dual-postgres-primary postgres { database = "eclair" host = "localhost" @@ -330,7 +371,7 @@ eclair { } safety-checks { // A set of basic checks on data to make sure we use the correct database - // Those checks are disabled by default because they wouldn't pass on a fresh new node with + // Those checks are disabled by default because they would not pass on a fresh new node with // zero channels. You should enable them when you already have channels, so that there is // something to compare to, and the values should be specific to your setup, especially // for local channels. If your operate a busy node, you can reduce max-age.local-channels @@ -348,6 +389,10 @@ eclair { } } } + dual { + migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) + compare-on-restart = false // compare sqlite and postgres dbs on restart (only applies if sqlite is primary) + } } file-backup { @@ -367,6 +412,8 @@ eclair { "blockstream.info", "mempool.space" ] + // maximum lag of chain height observed by eclair to blockchain watchdog sources + missing-blocks-threshold = 7 } onion-messages { @@ -382,7 +429,15 @@ eclair { max-per-peer-per-second = 10 # Consider a message to be lost if we haven't received a reply after that amount of time - reply-timeout = 20 seconds + reply-timeout = 5 seconds + + # If we expect a reply but do not get one, retry until we reach this number of attempts + max-attempts = 1 + } + + purge-expired-invoices { + enabled = true // enable automatic purges of expired invoices from the database + interval = 24 hours // interval between expired invoice purges } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala index f00c70f40b..0a22160df0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala @@ -36,6 +36,8 @@ case class BlockHeight(private val underlying: Long) extends Ordered[BlockHeight def toInt: Int = underlying.toInt def toLong: Long = underlying def toDouble: Double = underlying.toDouble + + override def toString() = underlying.toString // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala similarity index 56% rename from eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala index 9a3b7ef8e3..4119a1e393 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala @@ -16,31 +16,36 @@ package fr.acinq.eclair +import fr.acinq.eclair.channel.Commitments import grizzled.slf4j.Logging import scala.util.{Failure, Success, Try} -object DBCompatChecker extends Logging { +object DBChecker extends Logging { /** - * Tests if the channels data in the DB are compatible with the current version of eclair; throws an exception if incompatible. - * - * @param nodeParams - */ - def checkDBCompatibility(nodeParams: NodeParams): Unit = + * Tests if the channels data in the DB is valid (and throws an exception if not): + * - it is compatible with the current version of eclair + * - channel keys can be re-generated from the channel seed + * + * @param nodeParams node parameters + */ + def checkChannelsDB(nodeParams: NodeParams): Unit = { Try(nodeParams.db.channels.listLocalChannels()) match { - case Success(_) => {} + case Success(channels) => + channels.foreach(data => if (!Commitments.validateSeed(data.commitments, nodeParams.channelKeyManager)) throw InvalidChannelSeedException(data.channelId)) case Failure(_) => throw IncompatibleDBException } + } /** - * Tests if the network database is readable. - * - * @param nodeParams - */ - def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = + * Tests if the network database is readable. + * + * @param nodeParams + */ + def checkNetworkDB(nodeParams: NodeParams): Unit = Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes()) match { - case Success(_) => {} + case Success(_) => () case Failure(_) => throw IncompatibleNetworkDBException } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 662ff8dc3f..9b2bd00346 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair -import akka.actor.ActorRef -import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps +import akka.actor.{ActorRef, typed} +import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem} +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps} import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.ApiTypes.ChannelNotFound import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} @@ -40,16 +40,18 @@ import fr.acinq.eclair.io._ import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment -import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, RelayFees, UsableBalance} +import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, OutgoingChannels, RelayFees} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived +import fr.acinq.eclair.payment.send.OfferPayment import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector -import scodec.{Attempt, DecodeResult, codecs} +import scodec.{Attempt, DecodeResult} import java.nio.charset.StandardCharsets import java.util.UUID @@ -57,18 +59,18 @@ import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future, Promise} -case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features, chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) +case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[Feature], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) // @formatter:off case class SignedMessage(nodeId: PublicKey, message: String, signature: ByteVector) case class VerifiedMessage(valid: Boolean, publicKey: PublicKey) -// @formatter:on -// @formatter:off case class SendOnionMessageResponsePayload(encodedReplyPath: Option[String], replyPath: Option[Sphinx.RouteBlinding.BlindedRoute], unknownTlvs: Map[String, ByteVector]) case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload]) + +case class PayOfferResponse(invoice: Option[String], invoiceRequest: Option[String], payerKey: Option[String], preimage: Option[String], failureMessage: Option[String]) // @formatter:on object SignedMessage { @@ -88,7 +90,7 @@ trait Eclair { def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] + def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] @@ -96,23 +98,23 @@ trait Eclair { def updateRelayFee(nodes: List[PublicKey], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] - def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] + def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] - def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GETINFO]] + def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]] def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] - def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] + def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] def newAddress(): Future[String] def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] - def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] + def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] - def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] + def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] @@ -120,11 +122,11 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] + def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse] @@ -132,11 +134,11 @@ trait Eclair { def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] - def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] + def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] - def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] - def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] def deleteInvoice(paymentHash: ByteVector32): Future[String] @@ -146,7 +148,9 @@ trait Eclair { def getInfo()(implicit timeout: Timeout): Future[GetInfoResponse] - def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] + def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] + + def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] def onChainBalance(): Future[OnChainBalance] @@ -159,12 +163,18 @@ trait Eclair { def verifyMessage(message: ByteVector, recoverableSignature: ByteVector): VerifiedMessage def sendOnionMessage(intermediateNodes: Seq[PublicKey], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] + + def payOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[PayOfferResponse] + + def stop(): Future[Unit] } class EclairImpl(appKit: Kit) extends Eclair with Logging { implicit val ec: ExecutionContext = appKit.system.dispatcher + implicit val typedSystem: typed.ActorSystem[Nothing] = appKit.system.toTyped + // We constrain external identifiers. This allows uuid, long and pubkey to be used. private val externalIdMaxLength = 66 @@ -177,17 +187,21 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) - (appKit.switchboard ? Peer.OpenChannel( - remoteNodeId = nodeId, - fundingSatoshis = fundingAmount, - pushMsat = pushAmount_opt.getOrElse(0 msat), - channelType_opt = channelType_opt, - fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = flags_opt.map(_.toByte), - timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] + for { + _ <- Future.successful(0) + open = Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = fundingAmount, + pushMsat = pushAmount_opt.getOrElse(0 msat), + channelType_opt = channelType_opt, + fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), + timeout_opt = Some(openTimeout)) + res <- (appKit.switchboard ? open).mapTo[ChannelOpenResponse] + } yield res } override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { @@ -216,7 +230,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { .map(_.filter(n => nodeIds_opt.forall(_.contains(n.nodeId)))) } - override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = { + override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] = { val futureResponse = toRemoteNode_opt match { case Some(pk) => (appKit.register ? Symbol("channelsTo")).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) case None => (appKit.register ? Symbol("channels")).mapTo[Map[ByteVector32, ActorRef]].map(_.keys) @@ -224,14 +238,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { for { channelIds <- futureResponse - channels <- Future.sequence(channelIds.map(channelId => sendToChannel[CMD_GETINFO, CommandResponse[CMD_GETINFO]](Left(channelId), CMD_GETINFO(ActorRef.noSender)))) + channels <- Future.sequence(channelIds.map(channelId => sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](Left(channelId), CMD_GET_CHANNEL_INFO(ActorRef.noSender)))) } yield channels.collect { - case properResponse: RES_GETINFO => properResponse + case properResponse: RES_GET_CHANNEL_INFO => properResponse } } - override def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GETINFO]] = { - sendToChannel[CMD_GETINFO, CommandResponse[CMD_GETINFO]](channel, CMD_GETINFO(ActorRef.noSender)) + override def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]] = { + sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](channel, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) } override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = { @@ -249,9 +263,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { + override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] = { fallbackAddress_opt.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception - (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)).mapTo[PaymentRequest] + (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)).mapTo[Bolt11Invoice] } override def newAddress(): Future[String] = { @@ -282,8 +296,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, extraEdges, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -295,23 +309,23 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) for { ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) - response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] } yield response case Left(t) => Future.failed(t) } } - override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { - val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount)) - val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, finalCltvExpiryDelta, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) - if (invoice.isExpired) { + override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { + val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount)) + val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) + if (invoice.isExpired()) { Future.failed(new IllegalArgumentException("invoice has expired")) } else if (route.isEmpty) { Future.failed(new IllegalArgumentException("missing payment route")) @@ -326,7 +340,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, SendPaymentToNode] = { + private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, SendPaymentToNode] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) getRouteParams(pathFindingExperimentName_opt) match { case Right(defaultRouteParams) => @@ -335,24 +349,21 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { .modify(_.boundaries.maxFeeFlat).setToIfDefined(maxFeeFlat_opt.map(_.toMilliSatoshi)) externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) - case _ if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired")) - case _ => invoice.minFinalCltvExpiryDelta match { - case Some(minFinalCltvExpiryDelta) => Right(SendPaymentToNode(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) - case None => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) - } + case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) + case _ => Right(SendPaymentToNode(ActorRef.noSender ,amount, invoice, maxAttempts, externalId_opt, extraEdges = invoice.extraEdges, routeParams = routeParams)) } case Left(t) => Left(t) } } - override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { + override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID] } } - override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = { + override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = { createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).map { @@ -392,9 +403,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { val paymentType = "placeholder" val dummyOutgoingPayment = pending match { case PendingSpontaneousPayment(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), None, OutgoingPaymentStatus.Pending) - case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) - case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) - case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) + case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) + case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) + case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) } dummyOutgoingPayment +: outgoingDbPayments } @@ -424,16 +435,16 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli)) } - override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) + override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future { + appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) + override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future { + appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.paymentRequest) + override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] = Future { + appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.invoice) } override def deleteInvoice(paymentHash: ByteVector32): Future[String] = { @@ -446,8 +457,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { * @param channel either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded). */ private def sendToChannel[C <: Command, R <: CommandResponse[C]](channel: ApiTypes.ChannelIdentifier, request: C)(implicit timeout: Timeout): Future[R] = (channel match { - case Left(channelId) => appKit.register ? Register.Forward(ActorRef.noSender, channelId, request) - case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(ActorRef.noSender, shortChannelId, request) + case Left(channelId) => appKit.register ? Register.Forward(null, channelId, request) + case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(null, shortChannelId, request) }).map { case t: R@unchecked => t case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId)) @@ -487,8 +498,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { instanceId = appKit.nodeParams.instanceId.toString) ) - override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] = - (appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance)) + override def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] = + (appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance)) + + override def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] = + (appKit.relayer ? GetOutgoingChannels(enabledOnly = false)).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance)) override def globalBalance()(implicit timeout: Timeout): Future[GlobalBalance] = { for { @@ -537,19 +551,19 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] = { - codecs.list(TlvCodecs.genericTlv).decode(userCustomContent.bits) match { + MessageOnionCodecs.perHopPayloadCodec.decode(userCustomContent.bits) match { case Attempt.Successful(DecodeResult(userCustomTlvs, _)) => val replyPathId = randomBytes32() - val replyRoute = replyPath.map(hops => OnionMessages.buildRoute(randomKey(), hops.map(OnionMessages.IntermediateNode(_)), Left(OnionMessages.Recipient(appKit.nodeParams.nodeId, Some(replyPathId))))) + val replyRoute = replyPath.map(hops => OnionMessages.buildRoute(randomKey(), hops.map(OnionMessages.IntermediateNode(_)), OnionMessages.Recipient(appKit.nodeParams.nodeId, Some(replyPathId)))) val (nextNodeId, message) = OnionMessages.buildMessage( randomKey(), randomKey(), intermediateNodes.map(OnionMessages.IntermediateNode(_)), - destination match { case Left(key) => Left(OnionMessages.Recipient(key, None)) case Right(route) => Right(route) }, + destination match { case Left(key) => OnionMessages.Recipient(key, None) case Right(route) => OnionMessages.BlindedPath(route) }, replyRoute.map(OnionMessagePayloadTlv.ReplyPath(_) :: Nil).getOrElse(Nil), userCustomTlvs) appKit.postman.ask(ref => Postman.SendMessage(nextNodeId, message, replyPath.map(_ => replyPathId), ref, appKit.nodeParams.onionMessageConfig.timeout))(timeout, appKit.system.scheduler.toTyped).mapTo[Postman.OnionMessageResponse].map { - case Postman.Response(payload) => + case Postman.Response(payload, _) => val encodedReplyPath = payload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex) SendOnionMessageResponse(sent = true, None, Some(SendOnionMessageResponsePayload(encodedReplyPath, payload.replyPath.map(_.blindedRoute), payload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap))) case Postman.NoReply => SendOnionMessageResponse(sent = true, Some("No response"), None) @@ -558,6 +572,50 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } case Attempt.Failure(cause) => Future.successful(SendOnionMessageResponse(sent = false, failureMessage = Some(s"the `content` field is invalid, it must contain encoded tlvs: ${cause.message}"), response = None)) } + } + + override def payOffer(offer: Offer, + amount: MilliSatoshi, + quantity: Long, + externalId_opt: Option[String], + maxAttempts_opt: Option[Int], + maxFeeFlat_opt: Option[Satoshi], + maxFeePct_opt: Option[Double], + pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[PayOfferResponse] = { + if(externalId_opt.exists(_.length > externalIdMaxLength)) { + return Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) + } + val routeParams = getRouteParams(pathFindingExperimentName_opt) match { + case Right(defaultRouteParams) => + defaultRouteParams + .modify(_.boundaries.maxFeeProportional).setToIfDefined(maxFeePct_opt.map(_ / 100)) + .modify(_.boundaries.maxFeeFlat).setToIfDefined(maxFeeFlat_opt.map(_.toMilliSatoshi)) + case Left(t) => return Future.failed(t) + } + val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams) + val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.paymentInitiator, offer, amount, quantity, sendPaymentConfig)) + offerPayment.ask((ref: typed.ActorRef[OfferPayment.Result]) => OfferPayment.PayOffer(ref)).mapTo[OfferPayment.Result].map { + case OfferPayment.Success(invoice, payerKey, paymentPreimage) => PayOfferResponse(Some(invoice.toString), None, Some(payerKey.value.toHex), Some(paymentPreimage.toHex), None) + case OfferPayment.UnsupportedFeatures(features) => PayOfferResponse(None, None, None, None, Some(s"Unsupported features: $features")) + case OfferPayment.UnsupportedChains(chains) => PayOfferResponse(None, None, None, None, Some(s"Unsupported chains: ${chains.mkString(",")}")) + case OfferPayment.UnsupportedCurrency(iso4217) => PayOfferResponse(None, None, None, None, Some(s"Unsupported currency: $iso4217")) + case OfferPayment.ExpiredOffer(expiryDate) => PayOfferResponse(None, None, None, None, Some(s"Offer expired since $expiryDate")) + case OfferPayment.QuantityTooLow(quantityMin) => PayOfferResponse(None, None, None, None, Some(s"Minimum quantity is $quantityMin")) + case OfferPayment.QuantityTooHigh(quantityMax) => PayOfferResponse(None, None, None, None, Some(s"Maximum quantity is $quantityMax")) + case OfferPayment.IsSendInvoice => PayOfferResponse(None, None, None, None, Some("This offer is not payable")) + case OfferPayment.AmountInsufficient(amountNeeded) => PayOfferResponse(None, None, None, None, Some(s"Paying this offer requires at least $amountNeeded")) + case OfferPayment.InvalidSignature(signature) => PayOfferResponse(None, None, None, None, Some(s"Invalid signature: ${signature.toHex}")) + case OfferPayment.InvalidInvoice(invoice, request, payerKey) => PayOfferResponse(Some(invoice.toString), Some(request.toString), Some(payerKey.value.toHex), None, Some("Invalid invoice")) + case OfferPayment.NoInvoice => PayOfferResponse(None, None, None, None, Some("Couldn't retrieve invoice")) + case OfferPayment.FailedPayment(invoice, payerKey, paymentFailed) => PayOfferResponse(Some(invoice.toString), None, Some(payerKey.value.toHex), None, Some(paymentFailed.toString)) + } + } + override def stop(): Future[Unit] = { + // README: do not make this smarter or more complex ! + // eclair can simply and cleanly be stopped by killing its process without fear of losing data, payments, ... and it should remain this way. + logger.info("stopping eclair") + sys.exit(0) + Future.successful(()) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index aefea4bd6c..e6e260b0b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -20,6 +20,8 @@ import com.typesafe.config.Config import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import scodec.bits.{BitVector, ByteVector} +import scala.jdk.CollectionConverters.CollectionHasAsScala + /** * Created by PM on 13/02/2017. */ @@ -32,10 +34,9 @@ object FeatureSupport { case object Optional extends FeatureSupport { override def toString: String = "optional" } } +/** Not a sealed trait, so it can be extended by plugins. */ trait Feature { - this: FeatureScope => - def rfcName: String def mandatory: Int def optional: Int = mandatory + 1 @@ -46,24 +47,36 @@ trait Feature { } override def toString = rfcName - } /** Feature scope as defined in Bolt 9. */ -sealed trait FeatureScope /** Feature that should be advertised in init messages. */ -trait InitFeature extends FeatureScope +trait InitFeature extends Feature /** Feature that should be advertised in node announcements. */ -trait NodeFeature extends FeatureScope +trait NodeFeature extends Feature /** Feature that should be advertised in invoices. */ -trait InvoiceFeature extends FeatureScope +trait InvoiceFeature extends Feature +/** + * Feature negotiated when opening a channel that will apply for all of the channel's lifetime. + * This doesn't include features that can be safely activated/deactivated without impacting the channel's operation such + * as option_dataloss_protect or option_shutdown_anysegwit. + */ +trait PermanentChannelFeature extends InitFeature // <- not in the spec +/** + * Permanent channel feature negotiated in the channel type. Those features take precedence over permanent channel + * features negotiated in init messages. For example, if the channel type is option_static_remotekey, then even if + * the option_anchor_outputs feature is supported by both peers, it won't apply to the channel. + */ +trait ChannelTypeFeature extends PermanentChannelFeature // @formatter:on case class UnknownFeature(bitIndex: Int) -case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { +case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { + + def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty - def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match { + def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match { case Some(s) => activated.get(feature).contains(s) case None => activated.contains(feature) } @@ -71,7 +84,7 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow def hasPluginFeature(feature: UnknownFeature): Boolean = unknown.contains(feature) /** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */ - def areSupported(remoteFeatures: Features): Boolean = { + def areSupported(remoteFeatures: Features[T]): Boolean = { // we allow unknown odd features (it's ok to be odd) val unknownFeaturesOk = remoteFeatures.unknown.forall(_.bitIndex % 2 == 1) // we verify that we activated every mandatory feature they require @@ -82,12 +95,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow unknownFeaturesOk && knownFeaturesOk } - def initFeatures(): Features = Features(activated.collect { case (f: InitFeature, s) => (f: Feature, s) }, unknown) + def initFeatures(): Features[InitFeature] = Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown) + + def nodeAnnouncementFeatures(): Features[NodeFeature] = Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown) - def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown) + def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown) - // NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features. - def invoiceFeatures(): Map[Feature with InvoiceFeature, FeatureSupport] = activated.collect { case (f: InvoiceFeature, s) => (f, s) } + def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, unknown) def toByteVector: ByteVector = { val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) @@ -113,47 +127,38 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow object Features { - def empty = Features(Map.empty[Feature, FeatureSupport]) + def empty[T <: Feature]: Features[T] = Features[T](Map.empty[T, FeatureSupport]) - def apply(features: (Feature, FeatureSupport)*): Features = Features(Map.from(features)) + def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] = Features[T](Map.from(features)) - def apply(bytes: ByteVector): Features = apply(bytes.bits) + def apply(bytes: ByteVector): Features[Feature] = apply(bytes.bits) - def apply(bits: BitVector): Features = { + def apply(bits: BitVector): Features[Feature] = { val all = bits.toIndexedSeq.reverse.zipWithIndex.collect { case (true, idx) if knownFeatures.exists(_.optional == idx) => Right((knownFeatures.find(_.optional == idx).get, Optional)) case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right((knownFeatures.find(_.mandatory == idx).get, Mandatory)) case (true, idx) => Left(UnknownFeature(idx)) } - Features( + Features[Feature]( activated = all.collect { case Right((feature, support)) => feature -> support }.toMap, unknown = all.collect { case Left(inf) => inf }.toSet ) } - /** expects to have a top level config block named "features" */ - def fromConfiguration(config: Config): Features = Features( - knownFeatures.flatMap { - feature => - getFeature(config, feature.rfcName) match { - case Some(support) => Some(feature -> support) - case _ => None + def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T], baseFeatures: Features[T]): Features[T] = Features[T]( + config.root().entrySet().asScala.foldLeft(baseFeatures.activated) { + case (current, entry) => + val featureName = entry.getKey + val feature: T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) + config.getString(featureName) match { + case support if support == Mandatory.toString => current + (feature -> Mandatory) + case support if support == Optional.toString => current + (feature -> Optional) + case support if support == "disabled" => current - feature + case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") } - }.toMap) - - /** tries to extract the given feature name from the config, if successful returns its feature support */ - private def getFeature(config: Config, name: String): Option[FeatureSupport] = { - if (!config.hasPath(s"features.$name")) { - None - } else { - config.getString(s"features.$name") match { - case support if support == Mandatory.toString => Some(Mandatory) - case support if support == Optional.toString => Some(Optional) - case support if support == "disabled" => None - case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") - } - } - } + }) + + def fromConfiguration(config: Config): Features[Feature] = fromConfiguration[Feature](config, knownFeatures, Features.empty) case object DataLossProtect extends Feature with InitFeature with NodeFeature { val rfcName = "option_data_loss_protect" @@ -166,7 +171,7 @@ object Features { val mandatory = 2 } - case object UpfrontShutdownScript extends Feature with InitFeature with NodeFeature { + case object UpfrontShutdownScript extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_upfront_shutdown_script" val mandatory = 4 } @@ -186,7 +191,7 @@ object Features { val mandatory = 10 } - case object StaticRemoteKey extends Feature with InitFeature with NodeFeature { + case object StaticRemoteKey extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_static_remotekey" val mandatory = 12 } @@ -201,17 +206,17 @@ object Features { val mandatory = 16 } - case object Wumbo extends Feature with InitFeature with NodeFeature { + case object Wumbo extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_support_large_channel" val mandatory = 18 } - case object AnchorOutputs extends Feature with InitFeature with NodeFeature { + case object AnchorOutputs extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_anchor_outputs" val mandatory = 20 } - case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature { + case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_anchors_zero_fee_htlc_tx" val mandatory = 22 } @@ -221,6 +226,11 @@ object Features { val mandatory = 26 } + case object DualFunding extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { + val rfcName = "option_dual_fund" + val mandatory = 28 + } + case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 @@ -231,16 +241,18 @@ object Features { val mandatory = 44 } + case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_scid_alias" + val mandatory = 46 + } + case object PaymentMetadata extends Feature with InvoiceFeature { val rfcName = "option_payment_metadata" val mandatory = 48 } - // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) - // We're not advertising these bits yet in our announcements, clients have to assume support. - // This is why we haven't added them yet to `areSupported`. - case object TrampolinePayment extends Feature with InitFeature with NodeFeature with InvoiceFeature { - val rfcName = "trampoline_payment" + case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_zeroconf" val mandatory = 50 } @@ -249,6 +261,17 @@ object Features { val mandatory = 54 } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) + // We're not advertising these bits yet in our announcements, clients have to assume support. + // This is why we haven't added them yet to `areSupported`. + // The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted, + // we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely + // deprecate it. + case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with InvoiceFeature { + val rfcName = "trampoline_payment_prototype" + val mandatory = 148 + } + val knownFeatures: Set[Feature] = Set( DataLossProtect, InitialRoutingSync, @@ -263,38 +286,39 @@ object Features { AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, + DualFunding, OnionMessages, ChannelType, + ScidAlias, PaymentMetadata, - TrampolinePayment, - KeySend + ZeroConf, + KeySend, + TrampolinePaymentPrototype ) // Features may depend on other features, as specified in Bolt 9. private val featuresDependency = Map( ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil), - // This dependency requirement was added to the spec after the Phoenix release, which means Phoenix users have "invalid" - // invoices in their payment history. We choose to treat such invoices as valid; this is a harmless spec violation. - // PaymentSecret -> (VariableLengthOnion :: Nil), + PaymentSecret -> (VariableLengthOnion :: Nil), BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), - TrampolinePayment -> (PaymentSecret :: Nil), + TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) case class FeatureException(message: String) extends IllegalArgumentException(message) - def validateFeatureGraph(features: Features): Option[FeatureException] = featuresDependency.collectFirst { - case (feature, dependencies) if features.hasFeature(feature) && dependencies.exists(d => !features.hasFeature(d)) => - FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.hasFeature(d)).mkString(" and ")})") + def validateFeatureGraph[T <: Feature](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst { + case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) => + FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})") } /** Returns true if both feature sets are compatible. */ - def areCompatible(ours: Features, theirs: Features): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) + def areCompatible[T <: Feature](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) /** returns true if both have at least optional support */ - def canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean = { + def canUseFeature[T <: Feature](localFeatures: Features[T], remoteFeatures: Features[T], feature: T): Boolean = { localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala index 93d94c199f..46456d0521 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala @@ -21,8 +21,8 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.event.DiagnosticLoggingAdapter import akka.io.Tcp -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.ValidateResult import fr.acinq.eclair.channel.{LocalChannelDown, LocalChannelUpdate} import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted @@ -49,15 +49,18 @@ object Logs { parentPaymentId_opt: Option[UUID] = None, paymentId_opt: Option[UUID] = None, paymentHash_opt: Option[ByteVector32] = None, - txPublishId_opt: Option[UUID] = None): Map[String, String] = + txPublishId_opt: Option[UUID] = None, + nodeAlias_opt: Option[String] = None): Map[String, String] = Seq( + // nb: we preformat MDC values so that there is no white spaces in logs when they are not defined category_opt.map(l => "category" -> s" ${l.category}"), - remoteNodeId_opt.map(n => "nodeId" -> s" n:$n"), // nb: we preformat MDC values so that there is no white spaces in logs when they are not defined + remoteNodeId_opt.map(n => "nodeId" -> s" n:$n"), channelId_opt.map(c => "channelId" -> s" c:$c"), parentPaymentId_opt.map(p => "parentPaymentId" -> s" p:$p"), paymentId_opt.map(i => "paymentId" -> s" i:$i"), paymentHash_opt.map(h => "paymentHash" -> s" h:$h"), - txPublishId_opt.map(t => "txPublishId" -> s" t:$t") + txPublishId_opt.map(t => "txPublishId" -> s" t:$t"), + nodeAlias_opt.map(a => "nodeAlias" -> s" a:$a"), ).flatten.toMap /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala index f2e78ce589..c62933cd8d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} /** * Created by t-bast on 22/08/2019. */ /** - * One MilliSatoshi is a thousand of a Satoshi, the smallest unit usable in bitcoin + * One MilliSatoshi is a thousandth of a Satoshi, the smallest unit usable in bitcoin */ case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatoshi] { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e931c1d5f7..1d65b7a8c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel -import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy +import fr.acinq.eclair.channel.ChannelFlags +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} import fr.acinq.eclair.db._ @@ -58,48 +59,30 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, color: Color, publicAddresses: List[NodeAddress], torAddress_opt: Option[NodeAddress], - features: Features, - private val overrideFeatures: Map[PublicKey, Features], + features: Features[Feature], + private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]], syncWhitelist: Set[PublicKey], pluginParams: Seq[PluginParams], - dustLimit: Satoshi, - maxRemoteDustLimit: Satoshi, + channelConf: ChannelConf, onChainFeeConf: OnChainFeeConf, - maxHtlcValueInFlightMsat: UInt64, - maxAcceptedHtlcs: Int, - expiryDelta: CltvExpiryDelta, - fulfillSafetyBeforeTimeout: CltvExpiryDelta, - minFinalExpiryDelta: CltvExpiryDelta, - maxBlockProcessingDelay: FiniteDuration, - maxTxPublishRetryDelay: FiniteDuration, - htlcMinimum: MilliSatoshi, - toRemoteDelay: CltvExpiryDelta, - maxToLocalDelay: CltvExpiryDelta, - minDepthBlocks: Int, relayParams: RelayParams, - reserveToFundingRatio: Double, - maxReserveToFundingRatio: Double, - unhandledExceptionStrategy: UnhandledExceptionStrategy, db: Databases, - revocationTimeout: FiniteDuration, autoReconnect: Boolean, initialRandomReconnectDelay: FiniteDuration, maxReconnectInterval: FiniteDuration, chainHash: ByteVector32, - channelFlags: Byte, - watchSpentWindow: FiniteDuration, - paymentRequestExpiry: FiniteDuration, + invoiceExpiry: FiniteDuration, multiPartPaymentExpiry: FiniteDuration, - minFundingSatoshis: Satoshi, - maxFundingSatoshis: Satoshi, peerConnectionConf: PeerConnection.Conf, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams], maxPaymentAttempts: Int, enableTrampolinePayment: Boolean, balanceCheckInterval: FiniteDuration, + blockchainWatchdogThreshold: Int, blockchainWatchdogSources: Seq[String], - onionMessageConfig: OnionMessageConfig) { + onionMessageConfig: OnionMessageConfig, + purgeInvoicesInterval: Option[FiniteDuration]) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -111,7 +94,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get) /** Returns the features that should be used in our init message with the given peer. */ - def initFeaturesFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures() + def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures() } object NodeParams extends Logging { @@ -131,6 +114,7 @@ object NodeParams extends Logging { ConfigFactory.systemProperties() .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) .withFallback(ConfigFactory.load()) + .resolve() private def readSeedFromFile(seedPath: File): ByteVector = { logger.info(s"use seed file: ${seedPath.getCanonicalPath}") @@ -185,6 +169,22 @@ object NodeParams extends Logging { def chainFromHash(chainHash: ByteVector32): String = chain2Hash.map(_.swap).getOrElse(chainHash, throw new RuntimeException(s"invalid chainHash '$chainHash'")) + def parseSocks5ProxyParams(config: Config): Option[Socks5ProxyParams] = { + if (config.getBoolean("socks5.enabled")) { + Some(Socks5ProxyParams( + address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), + credentials_opt = None, + randomizeCredentials = config.getBoolean("socks5.randomize-credentials"), + useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), + useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), + useForTor = config.getBoolean("socks5.use-for-tor"), + useForWatchdogs = config.getBoolean("socks5.use-for-watchdogs"), + )) + } else { + None + } + } + def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feeEstimator: FeeEstimator, pluginParams: Seq[PluginParams] = Nil): NodeParams = { @@ -220,6 +220,33 @@ object NodeParams extends Logging { "router.path-finding.ratio-channel-capacity" -> "router.path-finding.default.ratios.channel-capacity", "router.path-finding.hop-cost-base-msat" -> "router.path-finding.default.hop-cost.fee-base-msat", "router.path-finding.hop-cost-millionths" -> "router.path-finding.default.hop-cost.fee-proportional-millionths", + // v0.6.3 + "channel-flags" -> "channel.channel-flags", + "dust-limit-satoshis" -> "channel.dust-limit-satoshis", + "max-remote-dust-limit-satoshis" -> "channel.max-remote-dust-limit-satoshis", + "htlc-minimum-msat" -> "channel.htlc-minimum-msat", + "max-htlc-value-in-flight-msat" -> "channel.max-htlc-value-in-flight-msat", + "max-accepted-htlcs" -> "channel.max-accepted-htlcs", + "reserve-to-funding-ratio" -> "channel.reserve-to-funding-ratio", + "max-reserve-to-funding-ratio" -> "channel.max-reserve-to-funding-ratio", + "min-funding-satoshis" -> "channel.min-funding-satoshis", + "max-funding-satoshis" -> "channel.max-funding-satoshis", + "to-remote-delay-blocks" -> "channel.to-remote-delay-blocks", + "max-to-local-delay-blocks" -> "channel.max-to-local-delay-blocks", + "mindepth-blocks" -> "channel.mindepth-blocks", + "expiry-delta-blocks" -> "channel.expiry-delta-blocks", + "fulfill-safety-before-timeout-blocks" -> "channel.fulfill-safety-before-timeout-blocks", + "min-final-expiry-delta-blocks" -> "channel.min-final-expiry-delta-blocks", + "max-block-processing-delay" -> "channel.max-block-processing-delay", + "max-tx-publish-retry-delay" -> "channel.max-tx-publish-retry-delay", + "unhandled-exception-strategy" -> "channel.unhandled-exception-strategy", + "revocation-timeout" -> "channel.revocation-timeout", + "watch-spent-window" -> "router.watch-spent-window", + // v0.7.1 + "payment-request-expiry" -> "invoice-expiry", + "override-features" -> "override-init-features", + "channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis", + "bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests" ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -232,37 +259,39 @@ object NodeParams extends Logging { val chain = config.getString("chain") val chainHash = hashFromChain(chain) + val channelFlags = ChannelFlags(announceChannel = config.getBoolean("channel.channel-flags.announce-channel")) + val color = ByteVector.fromValidHex(config.getString("node-color")) require(color.size == 3, "color should be a 3-bytes hex buffer") - val watchSpentWindow = FiniteDuration(config.getDuration("watch-spent-window").getSeconds, TimeUnit.SECONDS) - require(watchSpentWindow > 0.seconds, "watch-spent-window must be strictly greater than 0") + val watchSpentWindow = FiniteDuration(config.getDuration("router.watch-spent-window").getSeconds, TimeUnit.SECONDS) + require(watchSpentWindow > 0.seconds, "router.watch-spent-window must be strictly greater than 0") - val dustLimitSatoshis = Satoshi(config.getLong("dust-limit-satoshis")) + val dustLimitSatoshis = Satoshi(config.getLong("channel.dust-limit-satoshis")) if (chainHash == Block.LivenetGenesisBlock.hash) { require(dustLimitSatoshis >= Channel.MIN_DUST_LIMIT, s"dust limit must be greater than ${Channel.MIN_DUST_LIMIT}") } - val htlcMinimum = MilliSatoshi(config.getInt("htlc-minimum-msat")) - require(htlcMinimum > 0.msat, "htlc-minimum-msat must be strictly greater than 0") + val htlcMinimum = MilliSatoshi(config.getInt("channel.htlc-minimum-msat")) + require(htlcMinimum > 0.msat, "channel.htlc-minimum-msat must be strictly greater than 0") - val maxAcceptedHtlcs = config.getInt("max-accepted-htlcs") - require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}") + val maxAcceptedHtlcs = config.getInt("channel.max-accepted-htlcs") + require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"channel.max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}") - val maxToLocalCLTV = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")) - val offeredCLTV = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")) + val maxToLocalCLTV = CltvExpiryDelta(config.getInt("channel.max-to-local-delay-blocks")) + val offeredCLTV = CltvExpiryDelta(config.getInt("channel.to-remote-delay-blocks")) require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}") - val expiryDelta = CltvExpiryDelta(config.getInt("expiry-delta-blocks")) - val fulfillSafetyBeforeTimeout = CltvExpiryDelta(config.getInt("fulfill-safety-before-timeout-blocks")) - require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "fulfill-safety-before-timeout-blocks must be smaller than expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well") - val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("min-final-expiry-delta-blocks")) - require(minFinalExpiryDelta > fulfillSafetyBeforeTimeout, "min-final-expiry-delta-blocks must be strictly greater than fulfill-safety-before-timeout-blocks; otherwise it may lead to undesired channel closure") + val expiryDelta = CltvExpiryDelta(config.getInt("channel.expiry-delta-blocks")) + val fulfillSafetyBeforeTimeout = CltvExpiryDelta(config.getInt("channel.fulfill-safety-before-timeout-blocks")) + require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "channel.fulfill-safety-before-timeout-blocks must be smaller than channel.expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well") + val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("channel.min-final-expiry-delta-blocks")) + require(minFinalExpiryDelta > fulfillSafetyBeforeTimeout, "channel.min-final-expiry-delta-blocks must be strictly greater than channel.fulfill-safety-before-timeout-blocks; otherwise it may lead to undesired channel closure") val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") - def validateFeatures(features: Features): Unit = { + def validateFeatures(features: Features[Feature]): Unit = { val featuresErr = Features.validateFeatureGraph(features) require(featuresErr.isEmpty, featuresErr.map(_.message)) require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory") @@ -272,8 +301,9 @@ object NodeParams extends Logging { } val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p } - val features = Features.fromConfiguration(config) + val features = Features.fromConfiguration(config.getConfig("features")) validateFeatures(features) + require(!features.hasFeature(Features.ZeroConf), s"${Features.ZeroConf.rfcName} cannot be enabled for all peers: you have to use override-init-features to enable it on a per-peer basis") require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128") require(pluginMessageParams.forall(_.feature.mandatory % 2 == 0), "Plugin mandatory feature bit is odd, must be even") @@ -282,30 +312,18 @@ object NodeParams extends Logging { require(Features.knownFeatures.map(_.mandatory).intersect(pluginFeatureSet).isEmpty, "Plugin feature bit overlaps with known feature bit") require(pluginFeatureSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found") - val coreAndPluginFeatures = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) + val coreAndPluginFeatures: Features[Feature] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) - val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-features").asScala.map { e => + val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val f = Features.fromConfiguration(e) - validateFeatures(f) - p -> f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)) + val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }, features.initFeatures()) + validateFeatures(f.unscoped()) + p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature]) }.toMap val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet - val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { - Some(Socks5ProxyParams( - address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), - credentials_opt = None, - randomizeCredentials = config.getBoolean("socks5.randomize-credentials"), - useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), - useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), - useForTor = config.getBoolean("socks5.use-for-tor"), - useForWatchdogs = config.getBoolean("socks5.use-for-watchdogs"), - )) - } else { - None - } + val socksProxy_opt = parseSocks5ProxyParams(config) val publicTorAddress_opt = if (config.getBoolean("tor.publish-onion-address")) torAddress_opt else None @@ -351,6 +369,7 @@ object NodeParams extends Logging { lockedFundsRisk = config.getDouble("locked-funds-risk"), failureCost = getRelayFees(config.getConfig("failure-cost")), hopCost = getRelayFees(config.getConfig("hop-cost")), + useLogProbability = config.getBoolean("use-log-probability"), )) }, mpp = MultiPartParams( @@ -365,22 +384,23 @@ object NodeParams extends Logging { PathFindingExperimentConf(experiments.toMap) } - val unhandledExceptionStrategy = config.getString("unhandled-exception-strategy") match { + val unhandledExceptionStrategy = config.getString("channel.unhandled-exception-strategy") match { case "local-close" => UnhandledExceptionStrategy.LocalClose case "stop" => UnhandledExceptionStrategy.Stop } - val routerSyncEncodingType = config.getString("router.sync.encoding-type") match { - case "uncompressed" => EncodingType.UNCOMPRESSED - case "zlib" => EncodingType.COMPRESSED_ZLIB - } - val onionMessageRelayPolicy: RelayPolicy = config.getString("onion-messages.relay-policy") match { case "no-relay" => NoRelay case "channels-only" => RelayChannelsOnly case "relay-all" => RelayAll } + val purgeInvoicesInterval = if (config.getBoolean("purge-expired-invoices.enabled")) { + Some(FiniteDuration(config.getDuration("purge-expired-invoices.interval").toMinutes, TimeUnit.MINUTES)) + } else { + None + } + NodeParams( nodeKeyManager = nodeKeyManager, channelKeyManager = channelKeyManager, @@ -392,13 +412,35 @@ object NodeParams extends Logging { torAddress_opt = torAddress_opt, features = coreAndPluginFeatures, pluginParams = pluginParams, - overrideFeatures = overrideFeatures, + overrideInitFeatures = overrideInitFeatures, syncWhitelist = syncWhitelist, - dustLimit = dustLimitSatoshis, - maxRemoteDustLimit = Satoshi(config.getLong("max-remote-dust-limit-satoshis")), + channelConf = ChannelConf( + channelFlags = channelFlags, + dustLimit = dustLimitSatoshis, + maxRemoteDustLimit = Satoshi(config.getLong("channel.max-remote-dust-limit-satoshis")), + htlcMinimum = htlcMinimum, + maxHtlcValueInFlightMsat = UInt64(config.getLong("channel.max-htlc-value-in-flight-msat")), + maxAcceptedHtlcs = maxAcceptedHtlcs, + reserveToFundingRatio = config.getDouble("channel.reserve-to-funding-ratio"), + maxReserveToFundingRatio = config.getDouble("channel.max-reserve-to-funding-ratio"), + minFundingPublicSatoshis = Satoshi(config.getLong("channel.min-public-funding-satoshis")), + minFundingPrivateSatoshis = Satoshi(config.getLong("channel.min-private-funding-satoshis")), + maxFundingSatoshis = Satoshi(config.getLong("channel.max-funding-satoshis")), + toRemoteDelay = offeredCLTV, + maxToLocalDelay = maxToLocalCLTV, + minDepthBlocks = config.getInt("channel.mindepth-blocks"), + expiryDelta = expiryDelta, + fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout, + minFinalExpiryDelta = minFinalExpiryDelta, + maxBlockProcessingDelay = FiniteDuration(config.getDuration("channel.max-block-processing-delay").getSeconds, TimeUnit.SECONDS), + maxTxPublishRetryDelay = FiniteDuration(config.getDuration("channel.max-tx-publish-retry-delay").getSeconds, TimeUnit.SECONDS), + unhandledExceptionStrategy = unhandledExceptionStrategy, + revocationTimeout = FiniteDuration(config.getDuration("channel.revocation-timeout").getSeconds, TimeUnit.SECONDS) + ), onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, feeEstimator = feeEstimator, + spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio"), defaultFeerateTolerance = FeerateTolerance( @@ -424,37 +466,19 @@ object NodeParams extends Logging { nodeId -> tolerance }.toMap ), - maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")), - maxAcceptedHtlcs = maxAcceptedHtlcs, - expiryDelta = expiryDelta, - fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout, - minFinalExpiryDelta = minFinalExpiryDelta, - maxBlockProcessingDelay = FiniteDuration(config.getDuration("max-block-processing-delay").getSeconds, TimeUnit.SECONDS), - maxTxPublishRetryDelay = FiniteDuration(config.getDuration("max-tx-publish-retry-delay").getSeconds, TimeUnit.SECONDS), - htlcMinimum = htlcMinimum, - toRemoteDelay = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")), - maxToLocalDelay = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")), - minDepthBlocks = config.getInt("mindepth-blocks"), relayParams = RelayParams( publicChannelFees = getRelayFees(config.getConfig("relay.fees.public-channels")), privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")), minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")), + enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS) ), - reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), - maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), - unhandledExceptionStrategy = unhandledExceptionStrategy, db = database, - revocationTimeout = FiniteDuration(config.getDuration("revocation-timeout").getSeconds, TimeUnit.SECONDS), autoReconnect = config.getBoolean("auto-reconnect"), initialRandomReconnectDelay = FiniteDuration(config.getDuration("initial-random-reconnect-delay").getSeconds, TimeUnit.SECONDS), maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), chainHash = chainHash, - channelFlags = config.getInt("channel-flags").toByte, - watchSpentWindow = watchSpentWindow, - paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), + invoiceExpiry = FiniteDuration(config.getDuration("invoice-expiry").getSeconds, TimeUnit.SECONDS), multiPartPaymentExpiry = FiniteDuration(config.getDuration("multi-part-payment-expiry").getSeconds, TimeUnit.SECONDS), - minFundingSatoshis = Satoshi(config.getLong("min-funding-satoshis")), - maxFundingSatoshis = Satoshi(config.getLong("max-funding-satoshis")), peerConnectionConf = PeerConnection.Conf( authTimeout = FiniteDuration(config.getDuration("peer-connection.auth-timeout").getSeconds, TimeUnit.SECONDS), initTimeout = FiniteDuration(config.getDuration("peer-connection.init-timeout").getSeconds, TimeUnit.SECONDS), @@ -463,26 +487,32 @@ object NodeParams extends Logging { pingDisconnect = config.getBoolean("peer-connection.ping-disconnect"), maxRebroadcastDelay = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), // it makes sense to not delay rebroadcast by more than the rebroadcast period killIdleDelay = FiniteDuration(config.getDuration("onion-messages.kill-transient-connection-after").getSeconds, TimeUnit.SECONDS), - maxOnionMessagesPerSecond = config.getInt("onion-messages.max-per-peer-per-second") + maxOnionMessagesPerSecond = config.getInt("onion-messages.max-per-peer-per-second"), + sendRemoteAddressInit = config.getBoolean("peer-connection.send-remote-address-init"), ), routerConf = RouterConf( + watchSpentWindow = watchSpentWindow, channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), - encodingType = routerSyncEncodingType, + encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"), channelQueryChunkSize = config.getInt("router.sync.channel-query-chunk-size"), - pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")) + pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")), + balanceEstimateHalfLife = FiniteDuration(config.getDuration("router.balance-estimate-half-life").getSeconds, TimeUnit.SECONDS), ), socksProxy_opt = socksProxy_opt, maxPaymentAttempts = config.getInt("max-payment-attempts"), enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"), balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS), + blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"), blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq, onionMessageConfig = OnionMessageConfig( relayPolicy = onionMessageRelayPolicy, timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS), - ) + maxAttempts = config.getInt("onion-messages.max-attempts"), + ), + purgeInvoicesInterval = purgeInvoicesInterval ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala index 785c54e27d..feedf810e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.Origin import fr.acinq.eclair.payment.relay.PostRestartHtlcCleaner.IncomingHtlc diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 55aea8b6fe..643a93eb29 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -22,8 +22,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystem import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed} import akka.pattern.after import akka.util.Timeout -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.blockchain._ @@ -31,7 +30,8 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCAuthMethod} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.{Channel, Register} +import fr.acinq.eclair.channel.Register +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.WeakEntropyPool import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.db.Databases.FileBackup @@ -43,12 +43,12 @@ import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator} import fr.acinq.eclair.router._ -import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion import fr.acinq.eclair.tor.{Controller, TorProtocolHandler} import fr.acinq.eclair.wire.protocol.NodeAddress import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray import scodec.bits.ByteVector +import sttp.client3.okhttp.OkHttpFutureBackend import java.io.File import java.net.InetSocketAddress @@ -138,8 +138,8 @@ class Setup(val datadir: File, val serverBindingAddress = new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")) // early checks - DBCompatChecker.checkDBCompatibility(nodeParams) - DBCompatChecker.checkNetworkDBCompatibility(nodeParams) + DBChecker.checkChannelsDB(nodeParams) + DBChecker.checkNetworkDB(nodeParams) PortChecker.checkAvailable(serverBindingAddress) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") @@ -157,6 +157,7 @@ class Setup(val datadir: File, } case "password" => BitcoinJsonRPCAuthMethod.UserPassword(config.getString("bitcoind.rpcuser"), config.getString("bitcoind.rpcpassword")) } + val bitcoinClient = new BasicBitcoinJsonRPCClient( rpcAuthMethod = rpcAuthMethod, host = config.getString("bitcoind.host"), @@ -165,18 +166,22 @@ class Setup(val datadir: File, val future = for { json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) } // Make sure wallet support is enabled in bitcoind. - _ <- bitcoinClient.invoke("getbalance").recover { case e => throw BitcoinWalletDisabledException(e) } + wallets <- bitcoinClient.invoke("listwallets").recover { case e => throw BitcoinWalletDisabledException(e) } + .collect { + case JArray(values) => values.map(value => value.extract[String]) + } progress = (json \ "verificationprogress").extract[Double] ibd = (json \ "initialblockdownload").extract[Boolean] blocks = (json \ "blocks").extract[Long] headers = (json \ "headers").extract[Long] chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse) bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int]) - unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) => - values - .filter(value => (value \ "spendable").extract[Boolean]) - .map(value => (value \ "address").extract[String]) - } + unspentAddresses <- bitcoinClient.invoke("listunspent").recover { _ => if (wallet.isEmpty && wallets.length > 1) throw BitcoinDefaultWalletException(wallets) else throw BitcoinWalletNotLoadedException(wallet.getOrElse(""), wallets) } + .collect { case JArray(values) => + values + .filter(value => (value \ "spendable").extract[Boolean]) + .map(value => (value \ "address").extract[String]) + } _ <- chain match { case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 @@ -187,12 +192,12 @@ class Setup(val datadir: File, val (progress, initialBlockDownload, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") assert(bitcoinVersion >= 180000, "Eclair requires Bitcoin Core 0.18.0 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") + assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") if (chainHash != Block.RegtestGenesisBlock.hash) { - assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") + assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") + assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") + assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") } - assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") - assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") - assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") logger.info(s"current blockchain height=$blocks") blockHeight.set(blocks) bitcoinClient @@ -233,29 +238,33 @@ class Setup(val datadir: File, case _ => new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoin, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte) } - _ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(new Runnable { - override def run(): Unit = feeProvider.getFeerates.onComplete { - case Success(feerates) => - feeratesPerKB.set(feerates) - feeratesPerKw.set(FeeratesPerKw(feerates)) - channel.Monitoring.Metrics.LocalFeeratePerKw.withoutTags().update(feeratesPerKw.get.feePerBlock(nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget).toLong.toDouble) - blockchain.Monitoring.Metrics.MempoolMinFeeratePerKw.withoutTags().update(feeratesPerKw.get.mempoolMinFee.toLong.toDouble) - system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) - logger.info(s"current feeratesPerKB=${feeratesPerKB.get} feeratesPerKw=${feeratesPerKw.get}") - feeratesRetrieved.trySuccess(Done) - case Failure(exception) => - logger.warn(s"cannot retrieve feerates: ${exception.getMessage}") - blockchain.Monitoring.Metrics.CannotRetrieveFeeratesCount.withoutTags().increment() - feeratesRetrieved.tryFailure(CannotRetrieveFeerates) - } + _ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(() => feeProvider.getFeerates.onComplete { + case Success(feerates) => + feeratesPerKB.set(feerates) + feeratesPerKw.set(FeeratesPerKw(feerates)) + channel.Monitoring.Metrics.LocalFeeratePerKw.withoutTags().update(feeratesPerKw.get.feePerBlock(nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget).toLong.toDouble) + blockchain.Monitoring.Metrics.MempoolMinFeeratePerKw.withoutTags().update(feeratesPerKw.get.mempoolMinFee.toLong.toDouble) + system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) + logger.info(s"current feeratesPerKB=${feeratesPerKB.get} feeratesPerKw=${feeratesPerKw.get}") + feeratesRetrieved.trySuccess(Done) + case Failure(exception) => + logger.warn(s"cannot retrieve feerates: ${exception.getMessage}") + blockchain.Monitoring.Metrics.CannotRetrieveFeeratesCount.withoutTags().increment() + feeratesRetrieved.tryFailure(CannotRetrieveFeerates) }) _ <- feeratesRetrieved.future - bitcoinClient = new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + bitcoinClient = new BitcoinCoreClient(bitcoin) + watcher = { system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), ZMQActor.Topics.HashBlock, Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), ZMQActor.Topics.RawTx, Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) - system.spawn(Behaviors.supervise(ZmqWatcher(nodeParams, blockHeight, bitcoinClient)).onFailure(typed.SupervisorStrategy.resume), "watcher") + val watcherBitcoinClient = if (config.getBoolean("bitcoind.batch-watcher-requests")) { + new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + } else { + new BitcoinCoreClient(bitcoin) + } + system.spawn(Behaviors.supervise(ZmqWatcher(nodeParams, blockHeight, watcherBitcoinClient)).onFailure(typed.SupervisorStrategy.resume), "watcher") } router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher, Some(routerInitialized)), "router", SupervisorStrategy.Resume)) @@ -284,7 +293,7 @@ class Setup(val datadir: File, system.deadLetters } dbEventHandler = system.actorOf(SimpleSupervisor.props(DbEventHandler.props(nodeParams), "db-event-handler", SupervisorStrategy.Resume)) - register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume)) + register = system.actorOf(SimpleSupervisor.props(Register.props(), "register", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register), "payment-handler", SupervisorStrategy.Resume)) relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) // Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system, @@ -348,7 +357,6 @@ class Setup(val datadir: File, case "safecookie" => TorProtocolHandler.SafeCookie() } val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion(config.getString("tor.protocol")), authentication = auth, privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath, virtualPort = config.getInt("server.port"), @@ -404,10 +412,16 @@ case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could case class BitcoinRPCConnectionException(e: Throwable) extends RuntimeException("could not connect to bitcoind using json-rpc", e) -case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet not available", e) +case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet support disabled", e) + +case class BitcoinDefaultWalletException(loaded: List[String]) extends RuntimeException(s"no bitcoind wallet configured, but multiple wallets loaded: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}") + +case class BitcoinWalletNotLoadedException(wallet: String, loaded: List[String]) extends RuntimeException(s"configured wallet \"$wallet\" not in the set of loaded bitcoind wallets: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}") case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api") case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair") -case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair") \ No newline at end of file +case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair") + +case class InvalidChannelSeedException(channelId: ByteVector32) extends RuntimeException(s"channel seed has been modified for channel $channelId") \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala index 44fc67ff6c..3e33ff2118 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -16,46 +16,93 @@ package fr.acinq.eclair -/** - * A short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. - * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements - */ -case class ShortChannelId(private val id: Long) extends Ordered[ShortChannelId] { +import fr.acinq.eclair.ShortChannelId.toShortId - def toLong: Long = id +import scala.util.{Failure, Try} - def blockHeight = ShortChannelId.blockHeight(this) - - override def toString: String = { +// @formatter:off +sealed trait ShortChannelId extends Ordered[ShortChannelId] { + def toLong: Long + // we use an unsigned long comparison here + override def compare(that: ShortChannelId): Int = (this.toLong + Long.MinValue).compareTo(that.toLong + Long.MinValue) + override def hashCode(): Int = toLong.hashCode() + override def equals(obj: Any): Boolean = obj match { + case scid: ShortChannelId => this.toLong.equals(scid.toLong) + case _ => false + } + def toCoordinatesString: String = { val TxCoordinates(blockHeight, txIndex, outputIndex) = ShortChannelId.coordinates(this) s"${blockHeight.toLong}x${txIndex}x$outputIndex" } - - // we use an unsigned long comparison here - override def compare(that: ShortChannelId): Int = (this.id + Long.MinValue).compareTo(that.id + Long.MinValue) + def toHex: String = s"0x${toLong.toHexString}" +} +/** Sometimes we don't know what a scid really is */ +case class UnspecifiedShortChannelId(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toCoordinatesString // for backwards compatibility, because ChannelUpdate have an unspecified scid +} +case class RealShortChannelId private(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toCoordinatesString + def blockHeight: BlockHeight = ShortChannelId.blockHeight(this) + def outputIndex: Int = ShortChannelId.outputIndex(this) +} +case class Alias(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toHex } +// @formatter:on object ShortChannelId { + def apply(l: Long): ShortChannelId = UnspecifiedShortChannelId(l) - def apply(s: String): ShortChannelId = s.split("x").toList match { - case blockHeight :: txIndex :: outputIndex :: Nil => ShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) - case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s") - } + def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) - def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): ShortChannelId = ShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) + /** + * At block height 350 000 LN didn't exist, so all real scids less than that will never be used. This results in + * more than `2^58` values. + * + * The expected number of values before we have a collision can be approximated by (*): + * `Q(H) ~= sqrt( Pi * H / 2) = sqrt(Pi * 2^57) = 673 000 000` + * + * We don't expect to have anywhere close to that many channels at any given time on our node, so we don't need to + * check for duplicate values. + * + * (*) https://en.wikipedia.org/wiki/Birthday_attack + */ + private val aliasUpperBound = 2^58 - def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) + def generateLocalAlias(): Alias = { + // modulo won't skew the distribution because 2^64 is a multiple of 2^58 + val l = Math.abs(randomLong() % aliasUpperBound) + Alias(l) + } @inline - def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.id >> 40) & 0xFFFFFF) + def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.toLong >> 40) & 0xFFFFFF) @inline - def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.id >> 16) & 0xFFFFFF).toInt + def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.toLong >> 16) & 0xFFFFFF).toInt @inline - def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt + def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.toLong & 0xFFFF).toInt def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) + + def fromCoordinates(s: String): Try[ShortChannelId] = s.split("x").toList match { + case blockHeight :: txIndex :: outputIndex :: Nil => Try { + UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) + } + case _ => Failure(new IllegalArgumentException(s"Invalid short channel id: $s")) + } +} + +/** + * A real short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. + * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements + */ +object RealShortChannelId { + def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): RealShortChannelId = RealShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) } case class TxCoordinates(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int) \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala new file mode 100644 index 0000000000..23541c5263 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala @@ -0,0 +1,10 @@ +package fr.acinq.eclair + +/** + * Since actors are initialized asynchronously, and the initialization sometimes involves subscribing to an + * [[akka.event.EventStream]], we don't know when they are ready to process messages, especially in tests, which + * leads to race conditions. + * By making actors publish [[SubscriptionsComplete]] on the same [[akka.event.EventStream]] they are subscribing to, we guarantee + * that if we receive [[SubscriptionsComplete]] the actor has been initialized and its subscriptions have been taken into account. + */ +case class SubscriptionsComplete(clazz: Class[_]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index b46390d75a..3924d70f34 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.balance import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.balance.BalanceActor._ @@ -11,7 +11,7 @@ import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.Monitoring.{Metrics, Tags} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Utxo -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.Databases import grizzled.slf4j.Logger import org.json4s.JsonAST.JInt @@ -25,7 +25,7 @@ object BalanceActor { // @formatter:off sealed trait Command private final case object TickBalance extends Command - final case class GetGlobalBalance(replyTo: ActorRef[Try[GlobalBalance]], channels: Map[ByteVector32, HasCommitments]) extends Command + final case class GetGlobalBalance(replyTo: ActorRef[Try[GlobalBalance]], channels: Map[ByteVector32, PersistentChannelData]) extends Command private final case class WrappedChannels(wrapped: ChannelsListener.GetChannelsResponse) extends Command private final case class WrappedGlobalBalanceWithChannels(wrapped: Try[GlobalBalance], channelsCount: Int) extends Command private final case class WrappedUtxoInfo(wrapped: Try[UtxoInfo]) extends Command @@ -105,7 +105,7 @@ private class BalanceActor(context: ActorContext[Command], Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainConfirmed).update(result.onChain.confirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainUnconfirmed).update(result.onChain.unconfirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingConfirmed).update(result.offChain.waitForFundingConfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingLocked).update(result.offChain.waitForFundingLocked.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForChannelReady).update(result.offChain.waitForChannelReady.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.normal).update(result.offChain.normal.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.shutdown).update(result.offChain.shutdown.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingLocal).update(result.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala index 7e3a9dbd2a..64a41c3724 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala @@ -6,10 +6,10 @@ import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.balance.ChannelsListener._ import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.{ChannelPersisted, ChannelRestored, HasCommitments} +import fr.acinq.eclair.channel.{ChannelPersisted, ChannelRestored, PersistentChannelData} import scala.concurrent.Promise import scala.concurrent.duration.DurationInt @@ -18,14 +18,14 @@ object ChannelsListener { // @formatter:off sealed trait Command - private final case class ChannelData(channelId: ByteVector32, channel: akka.actor.ActorRef, data: HasCommitments) extends Command + private final case class ChannelData(channelId: ByteVector32, channel: akka.actor.ActorRef, data: PersistentChannelData) extends Command private final case class ChannelDied(channelId: ByteVector32) extends Command final case class GetChannels(replyTo: typed.ActorRef[GetChannelsResponse]) extends Command final case object SendDummyEvent extends Command final case object DummyEvent extends Command // @formatter:on - case class GetChannelsResponse(channels: Map[ByteVector32, HasCommitments]) + case class GetChannelsResponse(channels: Map[ByteVector32, PersistentChannelData]) def apply(ready: Promise[Done]): Behavior[Command] = Behaviors.setup { context => @@ -55,7 +55,7 @@ private class ChannelsListener(context: ActorContext[Command]) { private val log = context.log - def running(channels: Map[ByteVector32, HasCommitments]): Behavior[Command] = + def running(channels: Map[ByteVector32, PersistentChannelData]): Behavior[Command] = Behaviors.receiveMessage { case ChannelData(channelId, channel, data) => Closing.isClosed(data, additionalConfirmedTx_opt = None) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index dd76256f2c..7d08a54d09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.balance import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.{Btc, ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose, NextRemoteClose, RemoteClose} @@ -77,13 +77,13 @@ object CheckBalance { * The overall balance among all channels in all states. */ case class OffChainBalance(waitForFundingConfirmed: Btc = 0.sat, - waitForFundingLocked: Btc = 0.sat, + waitForChannelReady: Btc = 0.sat, normal: MainAndHtlcBalance = MainAndHtlcBalance(), shutdown: MainAndHtlcBalance = MainAndHtlcBalance(), negotiating: Btc = 0.sat, closing: ClosingBalance = ClosingBalance(), waitForPublishFutureCommitment: Btc = 0.sat) { - val total: Btc = waitForFundingConfirmed + waitForFundingLocked + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment + val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment } def updateMainBalance(localCommit: LocalCommit): Btc => Btc = { v: Btc => @@ -197,11 +197,11 @@ object CheckBalance { * - In the other cases, we simply take our local amount * - TODO?: we disregard anchor outputs */ - def computeOffChainBalance(channels: Iterable[HasCommitments], knownPreimages: Set[(ByteVector32, Long)]): OffChainBalance = { + def computeOffChainBalance(channels: Iterable[PersistentChannelData], knownPreimages: Set[(ByteVector32, Long)]): OffChainBalance = { channels .foldLeft(OffChainBalance()) { case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) - case (r, d: DATA_WAIT_FOR_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit)) + case (r, d: DATA_WAIT_FOR_CHANNEL_READY) => r.modify(_.waitForChannelReady).using(updateMainBalance(d.commitments.localCommit)) case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.localCommit)) @@ -293,7 +293,7 @@ object CheckBalance { val total: Btc = onChain.total + offChain.total } - def computeGlobalBalance(channels: Map[ByteVector32, HasCommitments], db: Databases, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { + def computeGlobalBalance(channels: Map[ByteVector32, PersistentChannelData], db: Databases, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { onChain <- CheckBalance.computeOnChainBalance(bitcoinClient) knownPreimages = db.pendingCommands.listSettlementCommands().collect { case (channelId, cmd: CMD_FULFILL_HTLC) => (channelId, cmd.id) }.toSet offChainRaw = CheckBalance.computeOffChainBalance(channels.values, knownPreimages) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala index cd672f27a4..0fb652c107 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala @@ -44,7 +44,7 @@ object Monitoring { object OffchainStates { val waitForFundingConfirmed = "waitForFundingConfirmed" - val waitForFundingLocked = "waitForFundingLocked" + val waitForChannelReady = "waitForChannelReady" val normal = "normal" val shutdown = "shutdown" val negotiating = "negotiating" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala index 55d1b54c46..03d815948b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.fee.FeeratesPerKw diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index 9a9df7532b..3f62f45009 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index c995f57260..c9331c651a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -19,13 +19,13 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.Monitoring.Metrics import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog import fr.acinq.eclair.wire.protocol.ChannelAnnouncement -import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, TimestampSecond} import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ @@ -136,8 +136,8 @@ object ZmqWatcher { /** This event is sent when a [[WatchSpentBasic]] condition is met. */ sealed trait WatchSpentBasicTriggered extends WatchTriggered - case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: ShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] - case class WatchExternalChannelSpentTriggered(shortChannelId: ShortChannelId) extends WatchSpentBasicTriggered + case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: RealShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] + case class WatchExternalChannelSpentTriggered(shortChannelId: RealShortChannelId) extends WatchSpentBasicTriggered case class WatchFundingSpent(replyTo: ActorRef[WatchFundingSpentTriggered], txId: ByteVector32, outputIndex: Int, hints: Set[ByteVector32]) extends WatchSpent[WatchFundingSpentTriggered] case class WatchFundingSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index 8137845375..3cb147f2d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -16,14 +16,15 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import com.softwaremill.sttp._ -import com.softwaremill.sttp.json4s._ -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.KamonExt import fr.acinq.eclair.blockchain.Monitoring.{Metrics, Tags} -import org.json4s.JsonAST.{JString, JValue} +import fr.acinq.eclair.json.{ByteVector32KmpSerializer, ByteVector32Serializer} +import org.json4s.DefaultFormats +import org.json4s.JsonAST.JValue import org.json4s.jackson.Serialization -import org.json4s.{CustomSerializer, DefaultFormats} +import sttp.client3._ +import sttp.client3.json4s._ +import sttp.model.StatusCode import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path} @@ -31,16 +32,10 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit http: SttpBackend[Future, Nothing]) extends BitcoinJsonRPCClient { +class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit sb: SttpBackend[Future, _]) extends BitcoinJsonRPCClient { - // necessary to properly serialize ByteVector32 into String readable by bitcoind - object ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { - null - }, { - case x: ByteVector32 => JString(x.toHex) - })) + implicit val formats = DefaultFormats.withBigDecimal + ByteVector32Serializer + ByteVector32KmpSerializer - implicit val formats = DefaultFormats.withBigDecimal + ByteVector32Serializer private val scheme = if (ssl) "https" else "http" private val serviceUri = wallet match { case Some(name) => uri"$scheme://$host:$port/wallet/$name" @@ -57,18 +52,19 @@ class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: S case o => o } - private def send(requests: Seq[JsonRPCRequest], user: String, password: String)(implicit ec: ExecutionContext): Future[Response[Seq[JsonRPCResponse]]] = { + private def send(requests: Seq[JsonRPCRequest], user: String, password: String)(implicit ec: ExecutionContext): Future[Response[Either[ResponseException[String, Exception], Seq[JsonRPCResponse]]]] = { requests.groupBy(_.method).foreach { case (method, calls) => Metrics.RpcBasicInvokeCount.withTag(Tags.Method, method).increment(calls.size) } - KamonExt.timeFuture(Metrics.RpcBasicInvokeDuration.withoutTags()) { + // for the duration metric, we use a "mixed" method for batched requests + KamonExt.timeFuture(Metrics.RpcBasicInvokeDuration.withTag(Tags.Method, if (requests.size == 1) requests.head.method else "mixed")) { for { - response <- sttp + response <- basicRequest .post(serviceUri) .body(requests) .auth.basic(user, password) .response(asJson[Seq[JsonRPCResponse]]) - .send() + .send(sb) } yield response } } @@ -78,18 +74,18 @@ class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: S send(requests, user, password).flatMap { response => response.code match { - case StatusCodes.Unauthorized => rpcAuthMethod match { + case StatusCode.Unauthorized => rpcAuthMethod match { case _: BitcoinJsonRPCAuthMethod.UserPassword => Future.failed(new IllegalArgumentException("could not authenticate to bitcoind RPC server: check your configured user/password")) case BitcoinJsonRPCAuthMethod.SafeCookie(path, _) => // bitcoind may have restarted and generated a new cookie file, let's read it again and retry BitcoinJsonRPCAuthMethod.readCookie(path) match { case Success(cookie) => credentials.set(cookie.credentials) - send(requests, cookie.credentials.user, cookie.credentials.password).map(_.unsafeBody) + send(requests, cookie.credentials.user, cookie.credentials.password).map(_.body.fold(exc => throw exc, jvalue => jvalue)) case Failure(e) => Future.failed(e) } } - case _ => Future.successful(response.unsafeBody) + case _ => Future.successful(response.body.fold(exc => throw exc, jvalue => jvalue)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 753a092883..896b1005fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Bech32, Block} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} @@ -32,6 +33,7 @@ import org.json4s.JsonAST._ import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.{Failure, Success, Try} /** @@ -93,11 +95,41 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall index = txs.indexOf(JString(txid.toHex)) } yield (BlockHeight(height.toInt), index) + /** + * Return true if this output can potentially be spent. + * + * Note that if this function returns false, that doesn't mean the output cannot be spent. The output could be unknown + * (not in the blockchain nor in the mempool) but could reappear later and be spendable at that point. If you want to + * ensure that an output is not spendable anymore, you should use [[isTransactionOutputSpent]]. + */ def isTransactionOutputSpendable(txid: ByteVector32, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = for { json <- rpcClient.invoke("gettxout", txid, outputIndex, includeMempool) } yield json != JNull + /** + * Return true if this output has already been spent by a confirmed transaction. + * Note that a reorg may invalidate the result of this function and make a spent output spendable again. + */ + def isTransactionOutputSpent(txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] = { + getTxConfirmations(txid).flatMap { + case Some(confirmations) if confirmations > 0 => + // There is an important limitation when using isTransactionOutputSpendable: if it returns false, it can mean a + // few different things: + // - the input has been spent + // - the input is coming from an unconfirmed transaction (in the mempool) but can be unspent + // - the input is unknown (it may come from an unconfirmed transaction that we don't have in our mempool) + // + // The only way to make sure that our output has been spent is to verify that it is coming from a confirmed + // transaction and that it has been spent by another confirmed transaction. We want to ignore the mempool to + // only consider spending transactions that have been confirmed. + isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map(r => !r) + case _ => + // If the output itself isn't in the blockchain, it cannot be spent by a confirmed transaction. + Future.successful(false) + } + } + def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = for { exists <- getTransaction(tx.txid) @@ -109,16 +141,20 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall false // won't be reached case _ => false } - doublespent <- if (exists) { - // if the tx is in the blockchain, it can't have been double-spent + doubleSpent <- if (exists) { + // if the tx is in the blockchain or in the mempool, it can't have been double-spent Future.successful(false) } else { - // if the tx wasn't in the blockchain and one of its inputs has been spent, it is double-spent - // NB: we don't look in the mempool, so it means that we will only consider that the tx has been double-spent if - // the overriding transaction has been confirmed - Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpendable(txIn.outPoint.txid, txIn.outPoint.index.toInt, includeMempool = false))).map(_.exists(_ == false)) + // The only way to make sure that our transaction has been double-spent is to find an input that is coming from + // a confirmed transaction and that it has been spent by another confirmed transaction. + // + // Note that if our transaction only had unconfirmed inputs and the transactions creating those inputs have + // themselves been double-spent, we will never be able to consider our transaction double-spent. With the + // information we have, these unknown inputs could eventually reappear and the transaction could be broadcast + // again. + Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpent(txIn.outPoint.txid, txIn.outPoint.index.toInt))).map(_.exists(_ == true)) } - } yield doublespent + } yield doubleSpent /** * Iterate over blocks to find the transaction that has spent a given output. @@ -130,7 +166,11 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall * @param outputIndex index of the transaction output that has been spent. * @return the transaction spending the given output. */ - def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = + def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = { + lookForSpendingTx(blockhash_opt.map(KotlinUtils.scala2kmp), KotlinUtils.scala2kmp(txid), outputIndex) + } + + def lookForSpendingTx(blockhash_opt: Option[fr.acinq.bitcoin.ByteVector32], txid: fr.acinq.bitcoin.ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = for { blockhash <- blockhash_opt match { case Some(b) => Future.successful(b) @@ -138,10 +178,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall } // with a verbosity of 0, getblock returns the raw serialized block block <- rpcClient.invoke("getblock", blockhash, 0).collect { case JString(b) => Block.read(b) } - prevblockhash = block.header.hashPreviousBlock.reverse - res <- block.tx.find(tx => tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match { + prevblockhash = block.header.hashPreviousBlock.reversed() + res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match { case None => lookForSpendingTx(Some(prevblockhash), txid, outputIndex) - case Some(tx) => Future.successful(tx) + case Some(tx) => Future.successful(KotlinUtils.kmp2scala(tx)) } } yield res @@ -329,7 +369,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall def getChangeAddress()(implicit ec: ExecutionContext): Future[ByteVector] = { rpcClient.invoke("getrawchangeaddress", "bech32").collect { case JString(changeAddress) => - val (_, _, pubkeyHash) = Bech32.decodeWitnessAddress(changeAddress) + val pubkeyHash = ByteVector.view(Bech32.decodeWitnessAddress(changeAddress).getThird) pubkeyHash } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala index 9fbc96b254..5373cc7947 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala @@ -16,10 +16,9 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import java.io.IOException - import org.json4s.JsonAST.JValue +import java.io.IOException import scala.concurrent.{ExecutionContext, Future} trait BitcoinJsonRPCClient { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala index cb34b2d74c..fe8384d6e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.zmq import akka.Done import akka.actor.{Actor, ActorLogging} -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction} import org.zeromq.ZMQ.Event import org.zeromq.{SocketType, ZContext, ZMQ, ZMsg} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala index 409ed49371..d26e177b30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient import org.json4s.DefaultFormats import org.json4s.JsonAST._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala deleted file mode 100644 index ff9e9f4a32..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import fr.acinq.eclair.db.FeeratesDb - -import scala.concurrent.{ExecutionContext, Future} - - -class DbFeeProvider(db: FeeratesDb, provider: FeeProvider)(implicit ec: ExecutionContext) extends FeeProvider { - - /** This method retrieves feerates from the provider, and store results in the database */ - override def getFeerates: Future[FeeratesPerKB] = - provider.getFeerates map { feerates => - db.addOrUpdateFeerates(feerates) - feerates - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala index 80b064bd6a..dfadb39f69 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import grizzled.slf4j.Logging import scala.concurrent.{ExecutionContext, Future} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index 25f436d9fd..ec1d33dbfd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.blockchain.CurrentFeerates import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} import fr.acinq.eclair.transactions.Transactions @@ -49,14 +49,14 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax channelType match { case ChannelTypes.Standard | ChannelTypes.StaticRemoteKey => proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputs | ChannelTypes.AnchorOutputsZeroFeeHtlcTx => + case ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => // when using anchor outputs, we allow any feerate: fees will be set with CPFP and RBF at broadcast time false } } } -case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { +case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 32e2b2cee5..dc887a559f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import scala.concurrent.Future diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 848ff033c3..8dd8488076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -19,13 +19,12 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import com.softwaremill.sttp.SttpBackend import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, NodeParams, NotificationsLogger} +import sttp.client3.SttpBackend import java.util.UUID import scala.concurrent.Future @@ -43,11 +42,6 @@ object BlockchainWatchdog { case class BlockHeaderAt(blockHeight: BlockHeight, blockHeader: BlockHeader) case object NoBlockReceivedTimer - trait SupportsTor { - /** Tor proxy connection parameters */ - def socksProxy_opt: Option[Socks5ProxyParams] - } - sealed trait BlockchainWatchdogEvent /** * We are missing too many blocks compared to one of our blockchain watchdogs. @@ -72,12 +66,13 @@ object BlockchainWatchdog { def apply(nodeParams: NodeParams, maxRandomDelay: FiniteDuration, blockTimeout: FiniteDuration = 15 minutes): Behavior[Command] = { Behaviors.setup { context => val socksProxy_opt = nodeParams.socksProxy_opt.flatMap(params => if (params.useForWatchdogs) Some(params) else None) - implicit val sttpBackend: SttpBackend[Future, Nothing] = ExplorerApi.createSttpBackend(socksProxy_opt) + implicit val sb: SttpBackend[Future, _] = ExplorerApi.createSttpBackend(socksProxy_opt) val explorers = Seq( - ExplorerApi.BlockstreamExplorer(socksProxy_opt), - ExplorerApi.BlockcypherExplorer(socksProxy_opt), - ExplorerApi.MempoolSpaceExplorer(socksProxy_opt) + ExplorerApi.BlockcypherExplorer(), + // NB: if there is a proxy, we assume it is a tor proxy + ExplorerApi.BlockstreamExplorer(useTorEndpoints = socksProxy_opt.isDefined), + ExplorerApi.MempoolSpaceExplorer(useTorEndpoints = socksProxy_opt.isDefined) ).filter { e => val enabled = nodeParams.blockchainWatchdogSources.contains(e.name) if (!enabled) { @@ -109,10 +104,10 @@ object BlockchainWatchdog { case CheckLatestHeaders(blockHeight) => val id = UUID.randomUUID() if (headersOverDnsEnabled) { - context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight.toLong}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) + context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) } explorers.foreach { explorer => - context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight.toLong}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) + context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) } Behaviors.same case headers@LatestHeaders(blockHeight, blockHeaders, source) => @@ -120,7 +115,7 @@ object BlockchainWatchdog { case h if h.isEmpty => 0 case _ => blockHeaders.map(_.blockHeight).max - blockHeight } - if (missingBlocks >= 6) { + if (missingBlocks >= nodeParams.blockchainWatchdogThreshold) { context.log.warn("{}: we are {} blocks late: we may be eclipsed from the bitcoin network", source, missingBlocks) context.system.eventStream ! EventStream.Publish(DangerousBlocksSkew(headers)) context.system.eventStream ! EventStream.Publish(NotifyNodeOperator(NotificationsLogger.Warning, s"we are $missingBlocks late according to $source: we may be eclipsed from the bitcoin network, check your bitcoind node.")) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala index 6a6c002d5e..25acb0b94c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala @@ -20,17 +20,19 @@ import akka.actor.ActorSystem import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import akka.pattern.after -import com.softwaremill.sttp.json4s.asJson -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import com.softwaremill.sttp.{StatusCodes, SttpBackend, SttpBackendOptions, Uri, UriContext, sttp} -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32} -import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders, SupportsTor} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.bitcoin.BlockHeader +import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders} import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, randomBytes} import org.json4s.JsonAST.{JArray, JInt, JObject, JString} import org.json4s.jackson.Serialization import org.json4s.{DefaultFormats, Serialization} +import sttp.client3._ +import sttp.client3.json4s._ +import sttp.client3.okhttp.OkHttpFutureBackend +import sttp.model.{StatusCode, Uri} import java.time.OffsetDateTime import scala.concurrent.duration.DurationInt @@ -47,6 +49,8 @@ object ExplorerApi { implicit val formats: DefaultFormats = DefaultFormats implicit val serialization: Serialization = Serialization + import fr.acinq.bitcoin.scalacompat.KotlinUtils.scala2kmp + sealed trait Explorer { // @formatter:off /** Explorer friendly-name. */ @@ -92,9 +96,9 @@ object ExplorerApi { } } - def createSttpBackend(socksProxy_opt: Option[Socks5ProxyParams]): SttpBackend[Future, Nothing] = { + def createSttpBackend(socksProxy_opt: Option[Socks5ProxyParams]): SttpBackend[Future, _] = { val options = SttpBackendOptions(connectionTimeout = 30.seconds, proxy = None) - val sttpBackendOptions = socksProxy_opt match { + val sttpBackendOptions: SttpBackendOptions = socksProxy_opt match { case Some(proxy) => val host = proxy.address.getHostString val port = proxy.address.getPort @@ -111,7 +115,7 @@ object ExplorerApi { * Query https://blockcypher.com/ to fetch block headers. * See https://www.blockcypher.com/dev/bitcoin/#introduction. */ - case class BlockcypherExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Explorer with SupportsTor { + case class BlockcypherExplorer()(implicit val sb: SttpBackend[Future, _]) extends Explorer { override val name = "blockcypher.com" override val baseUris = Map( Block.TestnetGenesisBlock.hash -> uri"https://api.blockcypher.com/v1/btc/test3", @@ -130,60 +134,58 @@ object ExplorerApi { } yield headers } - private def getTip(baseUri: Uri)(implicit ec: ExecutionContext, sb: SttpBackend[Future, Nothing]): Future[BlockHeight] = { + private def getTip(baseUri: Uri)(implicit ec: ExecutionContext): Future[BlockHeight] = { for { - tip <- sttp.readTimeout(30 seconds).get(baseUri) + tip <- basicRequest.readTimeout(30 seconds).get(baseUri) .headers(Socks5ProxyParams.FakeFirefoxHeaders) .response(asJson[JObject]) - .send() - .map(r => { - val JInt(latestHeight) = r.unsafeBody \ "height" + .send(sb) + .map(_.body.fold(exc => throw exc, jvalue => jvalue)) + .map(json => { + val JInt(latestHeight) = json \ "height" BlockHeight(latestHeight.toLong) }) } yield tip } - private def getHeader(baseUri: Uri, blockCount: Long)(implicit ec: ExecutionContext, sb: SttpBackend[Future, Nothing]): Future[Seq[BlockHeaderAt]] = for { - header <- sttp.readTimeout(30 seconds).get(baseUri.path(baseUri.path :+ "blocks" :+ blockCount.toString)) + private def getHeader(baseUri: Uri, blockCount: Long)(implicit ec: ExecutionContext): Future[Seq[BlockHeaderAt]] = for { + header <- basicRequest.readTimeout(30 seconds).get(baseUri.addPath("blocks", blockCount.toString)) .headers(Socks5ProxyParams.FakeFirefoxHeaders) .response(asJson[JObject]) - .send() - .map(r => r.code match { + .send(sb) + .map(r => r.body match { // HTTP 404 is a "normal" error: we're trying to lookup future blocks that haven't been mined. - case StatusCodes.NotFound => Seq.empty - case _ => r.unsafeBody \ "error" match { - case JString(error) if error == s"Block $blockCount not found." => Seq.empty - case _ => Seq(r.unsafeBody) - } + case Left(res: HttpError[_]) if res.statusCode == StatusCode.NotFound => Seq.empty + case Left(otherError) => throw otherError + case Right(json) if json \ "error" == JString(s"Block $blockCount not found.") => Seq.empty + case Right(block) => + val JInt(height) = block \ "height" + val JInt(version) = block \ "ver" + val JString(time) = block \ "time" + val JInt(bits) = block \ "bits" + val JInt(nonce) = block \ "nonce" + val previousBlockHash = (block \ "prev_block").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) + val merkleRoot = (block \ "mrkl_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) + val header = new BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) + Seq(BlockHeaderAt(BlockHeight(height.toLong), header)) }) - .map(blocks => blocks.map(block => { - val JInt(height) = block \ "height" - val JInt(version) = block \ "ver" - val JString(time) = block \ "time" - val JInt(bits) = block \ "bits" - val JInt(nonce) = block \ "nonce" - val previousBlockHash = (block \ "prev_block").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val merkleRoot = (block \ "mrkl_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) - BlockHeaderAt(BlockHeight(height.toLong), header) - })) } yield header } /** Explorer API based on Esplora: see https://github.com/Blockstream/esplora/blob/master/API.md. */ - sealed trait Esplora extends Explorer with SupportsTor { - implicit val sb: SttpBackend[Future, Nothing] + sealed trait Esplora extends Explorer { + implicit val sb: SttpBackend[Future, _] override def getLatestHeaders(baseUri: Uri, currentBlockHeight: BlockHeight)(implicit context: ActorContext[Command]): Future[LatestHeaders] = { implicit val ec: ExecutionContext = context.system.executionContext for { - headers <- sttp.readTimeout(10 seconds).get(baseUri.path(baseUri.path :+ "blocks")) + headers <- basicRequest.readTimeout(10 seconds).get(baseUri.addPath("blocks")) .response(asJson[JArray]) - .send() + .send(sb) .map(r => r.code match { // HTTP 404 is a "normal" error: we're trying to lookup future blocks that haven't been mined. - case StatusCodes.NotFound => Seq.empty - case _ => r.unsafeBody.arr + case StatusCode.NotFound => Seq.empty + case _ => r.body.fold(exc => throw exc, jvalue => jvalue).arr }) .map(blocks => blocks.map(block => { val JInt(height) = block \ "height" @@ -193,7 +195,7 @@ object ExplorerApi { val JInt(nonce) = block \ "nonce" val previousBlockHash = (block \ "previousblockhash").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) val merkleRoot = (block \ "merkle_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, time.toLong, bits.toLong, nonce.toLong) + val header = new BlockHeader(version.toLong, previousBlockHash, merkleRoot, time.toLong, bits.toLong, nonce.toLong) BlockHeaderAt(BlockHeight(height.toLong), header) })) .map(headers => LatestHeaders(currentBlockHeight, headers.filter(_.blockHeight >= currentBlockHeight).toSet, name)) @@ -202,36 +204,34 @@ object ExplorerApi { } /** Query https://blockstream.info/ to fetch block headers. */ - case class BlockstreamExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Esplora { + case class BlockstreamExplorer(useTorEndpoints: Boolean)(implicit val sb: SttpBackend[Future, _]) extends Esplora { override val name = "blockstream.info" - override val baseUris = socksProxy_opt match { - case Some(_) => - Map( - Block.TestnetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api" - ) - case None => - Map( - Block.TestnetGenesisBlock.hash -> uri"https://blockstream.info/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"https://blockstream.info/api" - ) + override val baseUris = if (useTorEndpoints) { + Map( + Block.TestnetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api" + ) + } else { + Map( + Block.TestnetGenesisBlock.hash -> uri"https://blockstream.info/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"https://blockstream.info/api" + ) } } /** Query https://mempool.space/ to fetch block headers. */ - case class MempoolSpaceExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Esplora { + case class MempoolSpaceExplorer(useTorEndpoints: Boolean)(implicit val sb: SttpBackend[Future, _]) extends Esplora { override val name = "mempool.space" - override val baseUris = socksProxy_opt match { - case Some(_) => - Map( - Block.TestnetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api" - ) - case None => - Map( - Block.TestnetGenesisBlock.hash -> uri"https://mempool.space/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"https://mempool.space/api" - ) + override val baseUris = if (useTorEndpoints) { + Map( + Block.TestnetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api" + ) + } else { + Map( + Block.TestnetGenesisBlock.hash -> uri"https://mempool.space/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"https://mempool.space/api" + ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala index 744a8b7d80..d65ab84670 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala @@ -22,7 +22,8 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.{ActorRef, Behavior} import akka.io.dns.{AAAARecord, DnsProtocol} import akka.io.{Dns, IO} -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.BlockHeaderAt import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 13ac0b1462..538f69d9ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -17,14 +17,14 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, Alias, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64} import scodec.bits.ByteVector import java.util.UUID @@ -53,7 +53,7 @@ case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState case object WAIT_FOR_FUNDING_CREATED extends ChannelState case object WAIT_FOR_FUNDING_SIGNED extends ChannelState case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState -case object WAIT_FOR_FUNDING_LOCKED extends ChannelState +case object WAIT_FOR_CHANNEL_READY extends ChannelState case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState case object NEGOTIATING extends ChannelState @@ -62,7 +62,6 @@ case object CLOSED extends ChannelState case object OFFLINE extends ChannelState case object SYNCING extends ChannelState case object WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT extends ChannelState -case object ERR_FUNDING_LOST extends ChannelState case object ERR_INFORMATION_LEAK extends ChannelState /* @@ -79,14 +78,16 @@ case object ERR_INFORMATION_LEAK extends ChannelState case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, - fundingTxFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, + fundingTxFeerate: FeeratePerKw, localParams: LocalParams, remote: ActorRef, remoteInit: Init, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, - channelType: SupportedChannelType) + channelType: SupportedChannelType) { + require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels") +} case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, @@ -96,7 +97,7 @@ case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init) -case class INPUT_RESTORED(data: HasCommitments) +case class INPUT_RESTORED(data: PersistentChannelData) /* .d8888b. .d88888b. 888b d888 888b d888 d8888 888b 888 8888888b. .d8888b. @@ -179,9 +180,9 @@ sealed trait CloseCommand extends HasReplyToCommand final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand -final case class CMD_GETSTATE(replyTo: ActorRef) extends HasReplyToCommand -final case class CMD_GETSTATEDATA(replyTo: ActorRef) extends HasReplyToCommand -final case class CMD_GETINFO(replyTo: ActorRef)extends HasReplyToCommand +final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand +final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand +final case class CMD_GET_CHANNEL_INFO(replyTo: ActorRef)extends HasReplyToCommand /* 88888888b. 8888888888 .d8888b. 88888888b. ,ad8888ba, 888b 88 .d8888b. 8888888888 .d8888b. @@ -197,7 +198,7 @@ final case class CMD_GETINFO(replyTo: ActorRef)extends HasReplyToCommand /** response to [[Command]] requests */ sealed trait CommandResponse[+C <: Command] sealed trait CommandSuccess[+C <: Command] extends CommandResponse[C] -sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: Throwable } +sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: T } /** generic responses */ final case class RES_SUCCESS[+C <: Command](cmd: C, channelId: ByteVector32) extends CommandSuccess[C] @@ -225,9 +226,9 @@ object HtlcResult { final case class RES_ADD_SETTLED[+O <: Origin, +R <: HtlcResult](origin: O, htlc: UpdateAddHtlc, result: R) extends CommandSuccess[CMD_ADD_HTLC] /** other specific responses */ -final case class RES_GETSTATE[+S <: ChannelState](state: S) extends CommandSuccess[CMD_GETSTATE] -final case class RES_GETSTATEDATA[+D <: ChannelData](data: D) extends CommandSuccess[CMD_GETSTATEDATA] -final case class RES_GETINFO(nodeId: PublicKey, channelId: ByteVector32, state: ChannelState, data: ChannelData) extends CommandSuccess[CMD_GETINFO] +final case class RES_GET_CHANNEL_STATE(state: ChannelState) extends CommandSuccess[CMD_GET_CHANNEL_STATE] +final case class RES_GET_CHANNEL_DATA[+D <: ChannelData](data: D) extends CommandSuccess[CMD_GET_CHANNEL_DATA] +final case class RES_GET_CHANNEL_INFO(nodeId: PublicKey, channelId: ByteVector32, state: ChannelState, data: ChannelData) extends CommandSuccess[CMD_GET_CHANNEL_INFO] /** * Those are not response to [[Command]], but to [[fr.acinq.eclair.io.Peer.OpenChannel]] @@ -252,19 +253,6 @@ object ChannelOpenResponse { 8888888P" d88P 888 888 d88P 888 */ -sealed trait ChannelData extends PossiblyHarmful { - def channelId: ByteVector32 -} - -case object Nothing extends ChannelData { - val channelId: ByteVector32 = ByteVector32.Zeroes -} - -sealed trait HasCommitments extends ChannelData { - val channelId: ByteVector32 = commitments.channelId - def commitments: Commitments -} - case class ClosingTxProposed(unsignedTx: ClosingTx, localClosingSigned: ClosingSigned) sealed trait CommitPublished { @@ -375,10 +363,25 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti } } -final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends ChannelData { +sealed trait ChannelData extends PossiblyHarmful { + def channelId: ByteVector32 +} + +sealed trait TransientChannelData extends ChannelData + +case object Nothing extends TransientChannelData { + val channelId: ByteVector32 = ByteVector32.Zeroes +} + +sealed trait PersistentChannelData extends ChannelData { + val channelId: ByteVector32 = commitments.channelId + def commitments: Commitments +} + +final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends ChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, @@ -386,11 +389,11 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32 remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: OpenChannel) extends ChannelData { + lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, @@ -398,12 +401,12 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: AcceptChannel) extends ChannelData { + lastSent: AcceptChannel) extends TransientChannelData { val channelId: ByteVector32 = temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, @@ -414,34 +417,59 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: FundingCreated) extends ChannelData + lastSent: FundingCreated) extends TransientChannelData final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm - deferred: Option[FundingLocked], - lastSent: Either[FundingCreated, FundingSigned]) extends ChannelData with HasCommitments -final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends ChannelData with HasCommitments + deferred: Option[ChannelReady], + lastSent: Either[FundingCreated, FundingSigned]) extends PersistentChannelData +final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, + shortIds: ShortIds, + lastSent: ChannelReady) extends PersistentChannelData + +sealed trait RealScidStatus { def toOption: Option[RealShortChannelId] } +object RealScidStatus { + /** The funding transaction has been confirmed but hasn't reached min_depth, we must be ready for a reorg. */ + case class Temporary(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** The funding transaction has been deeply confirmed. */ + case class Final(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** We don't know the status of the funding transaction. */ + case object Unknown extends RealScidStatus { override def toOption: Option[RealShortChannelId] = None } +} + +/** + * Short identifiers for the channel + * + * @param real the real scid, it may change if a reorg happens before the channel reaches 6 conf + * @param localAlias we must remember the alias that we sent to our peer because we use it to: + * - identify incoming [[ChannelUpdate]] at the connection level + * - route outgoing payments to that channel + * @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate + * routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]] + */ +case class ShortIds(real: RealScidStatus, + localAlias: Alias, + remoteAlias_opt: Option[Alias]) final case class DATA_NORMAL(commitments: Commitments, - shortChannelId: ShortChannelId, - buried: Boolean, + shortIds: ShortIds, channelAnnouncement: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], remoteShutdown: Option[Shutdown], - closingFeerates: Option[ClosingFeerates]) extends ChannelData with HasCommitments -final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends ChannelData with HasCommitments + closingFeerates: Option[ClosingFeerates]) extends PersistentChannelData +final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends PersistentChannelData final case class DATA_NEGOTIATING(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) - bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends ChannelData with HasCommitments { + bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends PersistentChannelData { require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation") - require(!commitments.localParams.isFunder || closingTxProposed.forall(_.nonEmpty), "funder must have at least one closing signature for every negotiation attempt because it initiates the closing") + require(!commitments.localParams.isInitiator || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing") } final case class DATA_CLOSING(commitments: Commitments, - fundingTx: Option[Transaction], // this will be non-empty if we are funder and we got in closing while waiting for our own tx to be published + fundingTx: Option[Transaction], // this will be non-empty if we are the initiator and we got in closing while waiting for our own tx to be published waitingSince: BlockHeight, // how long since we initiated the closing mutualCloseProposed: List[ClosingTx], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have mutualClosePublished: List[ClosingTx] = Nil, @@ -449,12 +477,12 @@ final case class DATA_CLOSING(commitments: Commitments, remoteCommitPublished: Option[RemoteCommitPublished] = None, nextRemoteCommitPublished: Option[RemoteCommitPublished] = None, futureRemoteCommitPublished: Option[RemoteCommitPublished] = None, - revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends ChannelData with HasCommitments { + revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends PersistentChannelData { val spendingTxs: List[Transaction] = mutualClosePublished.map(_.tx) ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx) require(spendingTxs.nonEmpty, "there must be at least one tx published in this state") } -final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends ChannelData with HasCommitments +final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends PersistentChannelData /** * @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these @@ -465,14 +493,14 @@ case class LocalParams(nodeId: PublicKey, fundingKeyPath: DeterministicWallet.KeyPath, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, - isFunder: Boolean, + isInitiator: Boolean, defaultFinalScriptPubKey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], - initFeatures: Features) + initFeatures: Features[InitFeature]) /** * @param initFeatures see [[LocalParams.initFeatures]] @@ -480,7 +508,7 @@ case class LocalParams(nodeId: PublicKey, case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, @@ -489,11 +517,14 @@ case class RemoteParams(nodeId: PublicKey, paymentBasepoint: PublicKey, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, - initFeatures: Features, + initFeatures: Features[InitFeature], shutdownScript: Option[ByteVector]) +case class ChannelFlags(announceChannel: Boolean) { + override def toString: String = s"ChannelFlags(announceChannel=$announceChannel)" +} object ChannelFlags { - val AnnounceChannel = 0x01.toByte - val Empty = 0x00.toByte + val Private: ChannelFlags = ChannelFlags(announceChannel = false) + val Public: ChannelFlags = ChannelFlags(announceChannel = true) } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 073ab32e6e..b41cffaa9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -17,12 +17,12 @@ package fr.acinq.eclair.channel import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} -import fr.acinq.eclair.{BlockHeight, ShortChannelId} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} +import fr.acinq.eclair.{BlockHeight, Features, ShortChannelId} /** * Created by PM on 17/08/2016. @@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} trait ChannelEvent -case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent +case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, commitTxFeerate: FeeratePerKw, fundingTxFeerate: Option[FeeratePerKw]) extends ChannelEvent // This trait can be used by non-standard channels to inject themselves into Register actor and thus make them usable for routing trait AbstractChannelRestored extends ChannelEvent { @@ -40,17 +40,36 @@ trait AbstractChannelRestored extends ChannelEvent { val remoteNodeId: PublicKey } -case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, data: HasCommitments) extends AbstractChannelRestored +case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, data: PersistentChannelData) extends AbstractChannelRestored case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent -case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, previousShortChannelId: Option[ShortChannelId]) extends ChannelEvent - -case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent +/** + * This event will be sent whenever a new scid is assigned to the channel, be it a real, local alias or remote alias. + */ +case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent + +case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent { + /** + * We always include the local alias because we must always be able to route based on it. + * However we only include the real scid if option_scid_alias is disabled, because we otherwise want to hide it. + */ + def scidsForRouting: Seq[ShortChannelId] = { + val canUseRealScid = commitments match { + case c: Commitments => !c.channelFeatures.hasFeature(Features.ScidAlias) + case _ => false + } + if (canUseRealScid) { + shortIds.real.toOption.toSeq :+ shortIds.localAlias + } else { + Seq(shortIds.localAlias) + } + } +} -case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent +case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent -case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey) extends ChannelEvent +case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent case class ChannelStateChanged(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, previousState: ChannelState, currentState: ChannelState, commitments_opt: Option[AbstractCommitments]) extends ChannelEvent @@ -58,7 +77,7 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent -case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, data: ChannelData, error: ChannelOpenError, isFatal: Boolean) extends ChannelEvent +case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, error: ChannelOpenError, isFatal: Boolean) extends ChannelEvent // NB: the fee should be set to 0 when we're not paying it. case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction, miningFee: Satoshi, desc: String) extends ChannelEvent @@ -66,9 +85,9 @@ case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction) extends ChannelEvent // NB: this event is only sent when the channel is available. -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: AbstractCommitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, commitments: AbstractCommitments) extends ChannelEvent -case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: HasCommitments) extends ChannelEvent +case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: PersistentChannelData) extends ChannelEvent case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, refundAtBlock: BlockHeight) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 790baafe0e..222726ed80 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64} @@ -86,7 +86,7 @@ case class RemoteCannotAffordFeesForNewHtlc (override val channelId: Byte case class InvalidHtlcPreimage (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id") case class UnknownHtlcId (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id") case class CannotExtractSharedSecret (override val channelId: ByteVector32, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}") -case class FundeeCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the funder should send update_fee messages") +case class NonInitiatorCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the initiator should send update_fee messages") case class CannotAffordFees (override val channelId: ByteVector32, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"can't pay the fee: missing=$missing reserve=$reserve fees=$fees") case class CannotSignWithoutChanges (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign when there are no changes") case class CannotSignBeforeRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign until next revocation hash is received") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 5a8408294e..7eacd3e5ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.{Feature, FeatureSupport, Features} +import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** * Created by t-bast on 24/06/2021. @@ -28,11 +28,11 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features} * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the * channel is upgraded or closed. */ -case class ChannelFeatures(features: Set[Feature]) { +case class ChannelFeatures(features: Set[PermanentChannelFeature]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - ChannelTypes.AnchorOutputsZeroFeeHtlcTx + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = features.contains(Features.ScidAlias), zeroConf = features.contains(Features.ZeroConf)) } else if (hasFeature(Features.AnchorOutputs)) { ChannelTypes.AnchorOutputs } else if (hasFeature(Features.StaticRemoteKey)) { @@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature]) { val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet val commitmentFormat: CommitmentFormat = channelType.commitmentFormat - def hasFeature(feature: Feature): Boolean = features.contains(feature) + def hasFeature(feature: PermanentChannelFeature): Boolean = features.contains(feature) override def toString: String = features.mkString(",") @@ -53,15 +53,21 @@ case class ChannelFeatures(features: Set[Feature]) { object ChannelFeatures { - def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features)) + def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features)) /** Enrich the channel type with other permanent features that will be applied to the channel. */ - def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = { - // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, - // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures: Seq[Feature] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) - val allFeatures = channelType.features.toSeq ++ availableFeatures - ChannelFeatures(allFeatures: _*) + def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): ChannelFeatures = { + val additionalPermanentFeatures = Features.knownFeatures.collect { + // If we both support 0-conf or scid_alias, we use it even if it wasn't in the channel-type. + case Features.ScidAlias if Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel => Some(Features.ScidAlias) + case Features.ZeroConf if Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroConf) => Some(Features.ZeroConf) + // Other channel-type features are negotiated in the channel-type, we ignore their value from the init message. + case _: ChannelTypeFeature => None + // We add all other permanent channel features that aren't negotiated as part of the channel-type. + case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f) + }.flatten + val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures + ChannelFeatures(allPermanentFeatures: _*) } } @@ -69,10 +75,13 @@ object ChannelFeatures { /** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */ sealed trait ChannelType { /** Features representing that channel type. */ - def features: Set[Feature] + def features: Set[_ <: InitFeature] } sealed trait SupportedChannelType extends ChannelType { + /** Known channel-type features */ + override def features: Set[ChannelTypeFeature] + /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ def paysDirectlyToWallet: Boolean @@ -84,50 +93,63 @@ object ChannelTypes { // @formatter:off case object Standard extends SupportedChannelType { - override def features: Set[Feature] = Set.empty + override def features: Set[ChannelTypeFeature] = Set.empty override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "standard" } case object StaticRemoteKey extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey) + override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey) override def paysDirectlyToWallet: Boolean = true override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "static_remotekey" } case object AnchorOutputs extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) + override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } - case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + case class AnchorOutputsZeroFeeHtlcTx(scidAlias: Boolean, zeroConf: Boolean) extends SupportedChannelType { + override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, + Some(Features.StaticRemoteKey), + Some(Features.AnchorOutputsZeroFeeHtlcTx) + ).flatten override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat - override def toString: String = "anchor_outputs_zero_fee_htlc_tx" + override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } - case class UnsupportedChannelType(featureBits: Features) extends ChannelType { - override def features: Set[Feature] = featureBits.activated.keySet + case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { + override def features: Set[InitFeature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" } // @formatter:on - private val features2ChannelType: Map[Features, SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set( + Standard, + StaticRemoteKey, + AnchorOutputs, + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)) .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap // NB: Bolt 2: features must exactly match in order to identify a channel type. - def fromFeatures(features: Features): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) - + def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ - def defaultFromFeatures(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = { - if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { - AnchorOutputsZeroFeeHtlcTx - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { + def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { + def canUse(feature: InitFeature): Boolean = Features.canUseFeature(localFeatures, remoteFeatures, feature) + + if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { + AnchorOutputsZeroFeeHtlcTx(scidAlias = canUse(Features.ScidAlias) && !announceChannel, zeroConf = canUse(Features.ZeroConf)) // alias feature is incompatible with public channel + } else if (canUse(Features.AnchorOutputs)) { AnchorOutputs - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) { + } else if (canUse(Features.StaticRemoteKey)) { StaticRemoteKey } else { Standard @@ -135,7 +157,7 @@ object ChannelTypes { } /** Check if a given channel type is compatible with our features. */ - def areCompatible(localFeatures: Features, remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match { + def areCompatible(localFeatures: Features[InitFeature], remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match { case _: UnsupportedChannelType => None // We ensure that we support the features necessary for this channel type. case proposedChannelType: SupportedChannelType => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index f8ba682fe3..2294a09a6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.Metrics +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.crypto.{Generators, ShaChain} import fr.acinq.eclair.payment.OutgoingPaymentPacket @@ -74,7 +75,7 @@ case class Commitments(channelId: ByteVector32, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: Byte, + channelFlags: ChannelFlags, localCommit: LocalCommit, remoteCommit: RemoteCommit, localChanges: LocalChanges, remoteChanges: RemoteChanges, localNextHtlcId: Long, remoteNextHtlcId: Long, @@ -85,6 +86,8 @@ case class Commitments(channelId: ByteVector32, require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures") + def nextRemoteCommit_opt: Option[RemoteCommit] = remoteNextCommitInfo.swap.toOption.map(_.nextRemoteCommit) + /** * * @param scriptPubKey optional local script pubkey provided in CMD_CLOSE @@ -95,7 +98,7 @@ case class Commitments(channelId: ByteVector32, val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) (channelFeatures.hasFeature(Features.UpfrontShutdownScript), scriptPubKey) match { case (true, Some(script)) if script != localParams.defaultFinalScriptPubKey => Left(InvalidFinalScript(channelId)) - case (false, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, Some(script)) if !Closing.MutualClose.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (false, Some(script)) => Right(script) case _ => Right(localParams.defaultFinalScriptPubKey) } @@ -110,9 +113,9 @@ case class Commitments(channelId: ByteVector32, // to check whether shutdown_any_segwit is active we check features in local and remote parameters, which are negotiated each time we connect to our peer. val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) (channelFeatures.hasFeature(Features.UpfrontShutdownScript), remoteParams.shutdownScript) match { - case (false, _) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, _) if !Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (false, _) => Right(remoteScriptPubKey) - case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => + case (true, None) if !Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => // this is a special case: they set option_upfront_shutdown_script but did not provide a script in their open/accept message Left(InvalidFinalScript(channelId)) case (true, None) => Right(remoteScriptPubKey) @@ -204,12 +207,18 @@ case class Commitments(channelId: ByteVector32, val remoteNodeId: PublicKey = remoteParams.nodeId - val announceChannel: Boolean = (channelFlags & 0x01) != 0 + val announceChannel: Boolean = channelFlags.announceChannel val capacity: Satoshi = commitInput.txOut.amount - // NB: when computing availableBalanceForSend and availableBalanceForReceive, the funder keeps an extra buffer on top - // of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see + /** Channel reserve that applies to our funds. */ + val localChannelReserve: Satoshi = remoteParams.requestedChannelReserve_opt.getOrElse(0 sat) + + /** Channel reserve that applies to our peer's funds. */ + val remoteChannelReserve: Satoshi = localParams.requestedChannelReserve_opt.getOrElse(0 sat) + + // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on + // top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see // https://github.com/lightningnetwork/lightning-rfc/issues/728 for details). // // This extra buffer (which we call "funder fee buffer") is calculated as follows: @@ -238,11 +247,11 @@ case class Commitments(channelId: ByteVector32, // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation val remoteCommit1 = remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, remoteChanges.acked, localChanges.proposed) - val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) - if (localParams.isFunder) { - // The funder always pays the on-chain fees, so we must subtract that from the amount we can send. + val balanceNoFees = (reduced.toRemote - localChannelReserve).max(0 msat) + if (localParams.isInitiator) { + // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) - // the funder needs to keep a "funder fee buffer" (see explanation above) + // the initiator needs to keep a "funder fee buffer" (see explanation above) val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) { @@ -257,21 +266,21 @@ case class Commitments(channelId: ByteVector32, (balanceNoFees - amountToReserve1).max(0 msat) } } else { - // The fundee doesn't pay on-chain fees. + // The non-initiator doesn't pay on-chain fees. balanceNoFees } } lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val balanceNoFees = (reduced.toRemote - localParams.channelReserve).max(0 msat) - if (localParams.isFunder) { - // The fundee doesn't pay on-chain fees so we don't take those into account when receiving. + val balanceNoFees = (reduced.toRemote - remoteChannelReserve).max(0 msat) + if (localParams.isInitiator) { + // The non-initiator doesn't pay on-chain fees so we don't take those into account when receiving. balanceNoFees } else { - // The funder always pays the on-chain fees, so we must subtract that from the amount we can receive. + // The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive. val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat) - // we expected the funder to keep a "funder fee buffer" (see explanation above) + // we expected the initiator to keep a "funder fee buffer" (see explanation above) val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) { @@ -291,6 +300,13 @@ case class Commitments(channelId: ByteVector32, object Commitments { + def validateSeed(commitments: Commitments, keyManager: ChannelKeyManager): Boolean = { + val localFundingKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val remoteFundingKey = commitments.remoteParams.fundingPubKey + val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) + commitments.commitInput.redeemScript == fundingScript + } + /** * Add a change to our proposed change list. * @@ -355,22 +371,22 @@ object Commitments { // the HTLC we are about to create is outgoing, but from their point of view it is incoming val outgoingHtlcs = reduced.htlcs.collect(incoming) - // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - // the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid + // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. - val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) - val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) + val missingForSender = reduced.toRemote - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) + val missingForReceiver = reduced.toLocal - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) if (missingForSender < 0.msat) { - return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isFunder) fees else 0.sat)) + return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) } else if (missingForReceiver < 0.msat) { - if (commitments.localParams.isFunder) { - // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + if (commitments.localParams.isInitiator) { + // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } else { - return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = fees)) } } @@ -427,19 +443,19 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) val incomingHtlcs = reduced.htlcs.collect(incoming) - // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place + // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. - val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) - val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat) + val missingForSender = reduced.toRemote - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForReceiver = reduced.toLocal - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) if (missingForSender < 0.sat) { - return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isFunder) 0.sat else fees)) + return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) } else if (missingForReceiver < 0.sat) { - if (commitments.localParams.isFunder) { - return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + if (commitments.localParams.isInitiator) { + return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = fees)) } else { - // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } } @@ -536,8 +552,8 @@ object Commitments { } def sendFee(commitments: Commitments, cmd: CMD_UPDATE_FEE, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateFee)] = { - if (!commitments.localParams.isFunder) { - Left(FundeeCannotSendUpdateFee(commitments.channelId)) + if (!commitments.localParams.isInitiator) { + Left(NonInitiatorCannotSendUpdateFee(commitments.channelId)) } else { // let's compute the current commitment *as seen by them* with this change taken into account val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw) @@ -546,11 +562,11 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.remoteCommit.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - // we look from remote's point of view, so if local is funder remote doesn't pay the fees + // we look from remote's point of view, so if local is initiator remote doesn't pay the fees val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we avoid sending this fee update @@ -575,8 +591,8 @@ object Commitments { } def receiveFee(commitments: Commitments, fee: UpdateFee, feeConf: OnChainFeeConf)(implicit log: LoggingAdapter): Either[ChannelException, Commitments] = { - if (commitments.localParams.isFunder) { - Left(FundeeCannotSendUpdateFee(commitments.channelId)) + if (commitments.localParams.isInitiator) { + Left(NonInitiatorCannotSendUpdateFee(commitments.channelId)) } else if (fee.feeratePerKw < FeeratePerKw.MinimumFeeratePerKw) { Left(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw)) } else { @@ -586,7 +602,7 @@ object Commitments { if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) { Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)) } else { - // NB: we check that the funder can afford this new fee even if spec allows to do it at next signature + // NB: we check that the initiator can afford this new fee even if spec allows to do it at next signature // It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid, // and it would be tricky to check if the conditions are met at signing // (it also means that we need to check the fee of the initial commitment tx somewhere) @@ -598,9 +614,9 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee val fees = commitTxTotalCost(commitments1.localParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we reject this fee update @@ -842,8 +858,8 @@ object Commitments { val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) + val outputs = makeCommitTxOutputs(localParams.isInitiator, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isInitiator, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } @@ -869,8 +885,8 @@ object Commitments { val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs) + val outputs = makeCommitTxOutputs(!localParams.isInitiator, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isInitiator, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } @@ -899,7 +915,7 @@ object Commitments { case _: CommitSig => s"sig" case _: RevokeAndAck => s"rev" case _: Error => s"err" - case _: FundingLocked => s"funding_locked" + case _: ChannelReady => s"channel_ready" case _ => "???" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala index 84c68182b3..2279e87ec1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.transactions.Transactions.CommitmentFormat diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 5893d59575..a6f0b3b2a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -17,13 +17,14 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.OnChainAddressGenerator -import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} -import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw, OnChainFeeConf} +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.ChannelsDb @@ -37,7 +38,6 @@ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext} import scala.util.{Failure, Success, Try} /** @@ -48,13 +48,13 @@ object Helpers { /** * We update local/global features at reconnection */ - def updateFeatures(data: HasCommitments, localInit: Init, remoteInit: Init): HasCommitments = { + def updateFeatures(data: PersistentChannelData, localInit: Init, remoteInit: Init): PersistentChannelData = { val commitments1 = data.commitments.copy( localParams = data.commitments.localParams.copy(initFeatures = localInit.features), remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features)) data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1) case d: DATA_NORMAL => d.copy(commitments = commitments1) case d: DATA_SHUTDOWN => d.copy(commitments = commitments1) case d: DATA_NEGOTIATING => d.copy(commitments = commitments1) @@ -63,23 +63,7 @@ object Helpers { } } - /** - * Returns the number of confirmations needed to safely handle the funding transaction, - * we make sure the cumulative block reward largely exceeds the channel size. - * - * @param fundingSatoshis funding amount of the channel - * @return number of confirmations needed - */ - def minDepthForFunding(nodeParams: NodeParams, fundingSatoshis: Satoshi): Long = fundingSatoshis match { - case funding if funding <= Channel.MAX_FUNDING => nodeParams.minDepthBlocks - case funding => - val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 - val scalingFactor = 15 - val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt - nodeParams.minDepthBlocks.max(blocksToReachFunding) - } - - def extractShutdownScript(channelId: ByteVector32, localFeatures: Features, remoteFeatures: Features, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { + def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript) val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt) @@ -89,7 +73,7 @@ object Helpers { (hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match { case (true, None) => Left(MissingUpfrontShutdownScript(channelId)) case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty - case (true, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (true, Some(script)) if !Closing.MutualClose.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (true, Some(script)) => Right(Some(script)) case (false, Some(_)) => Right(None) // they provided a script but the feature is not active, we just ignore it case _ => Right(None) @@ -99,21 +83,21 @@ object Helpers { /** * Called by the fundee */ - def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) - if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis)) + if (open.fundingSatoshis < nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel) || open.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), nodeParams.channelConf.maxFundingSatoshis)) // BOLT #2: Channel funding limits - if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING)) + if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Channel.MAX_FUNDING)) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi)) // BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large. - if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.maxToLocalDelay)) + if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483. if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) @@ -121,7 +105,7 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing. if (isFeeTooSmall(open.feeratePerKw)) return Left(FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw)) - if (open.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.maxRemoteDustLimit)) + if (open.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit)) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (open.dustLimitSatoshis > open.channelReserveSatoshis) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, open.channelReserveSatoshis)) @@ -145,16 +129,16 @@ object Helpers { // now, but it will be done later when we receive `funding_created` val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) - if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) + if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) - val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } /** * Called by the funder */ - def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, remoteFeatures: Features, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { accept.channelType_opt match { case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => // if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel. @@ -162,10 +146,10 @@ object Helpers { case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` return Left(MissingChannelType(open.temporaryChannelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures) => + case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures))) + return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel))) case _ => // we agree on channel type } @@ -175,14 +159,14 @@ object Helpers { if (accept.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUST_LIMIT)) } - if (accept.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.maxRemoteDustLimit)) + if (accept.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit)) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (accept.dustLimitSatoshis > accept.channelReserveSatoshis) return Left(DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, accept.channelReserveSatoshis)) // if minimum_depth is unreasonably large: // MAY reject the channel. - if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.maxToLocalDelay)) + if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) // if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message: // MUST reject the channel. @@ -193,12 +177,38 @@ object Helpers { if (open.channelReserveSatoshis < accept.dustLimitSatoshis) return Left(DustLimitAboveOurChannelReserve(accept.temporaryChannelId, accept.dustLimitSatoshis, open.channelReserveSatoshis)) val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) - if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) + if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) - val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } + /** + * We use the real scid if the channel has been announced, otherwise we use our local alias. + */ + def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], localAlias: Alias): ShortChannelId = { + channelAnnouncement_opt.map(_.shortChannelId).getOrElse(localAlias) + } + + def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds.localAlias) + + /** + * If our peer sent us an alias, that's what we must use in the channel_update we send them to ensure they're able to + * match this update with the corresponding local channel. If they didn't send us an alias, it means we're not using + * 0-conf and we'll use the real scid. + */ + def channelUpdateForDirectPeer(nodeParams: NodeParams, channelUpdate: ChannelUpdate, shortIds: ShortIds): ChannelUpdate = { + shortIds.remoteAlias_opt match { + case Some(remoteAlias) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, remoteAlias) + case None => shortIds.real.toOption match { + case Some(realScid) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, realScid) + // This case is a spec violation: this is a 0-conf channel, so our peer MUST send their alias. + // They won't be able to match our channel_update with their local channel, too bad for them. + case None => channelUpdate + } + } + } + /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by * other nodes. @@ -224,8 +234,8 @@ object Helpers { remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw } - def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { - val features = Features.empty // empty features for now + def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: RealShortChannelId): AnnouncementSignatures = { + val features = Features.empty[Feature] // empty features for now val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val witness = Announcements.generateChannelAnnouncementWitness( nodeParams.chainHash, @@ -252,23 +262,11 @@ object Helpers { } val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi // NB: this is an approximation (we don't take network fees into account) - val result = toRemoteSatoshis > commitments.remoteParams.channelReserve - log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") + val result = toRemoteSatoshis > commitments.localChannelReserve + log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.localChannelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") result } - /** NB: this is a blocking call, use carefully! */ - def getFinalScriptPubKey(wallet: OnChainAddressGenerator, chainHash: ByteVector32)(implicit ec: ExecutionContext): ByteVector = { - import scala.concurrent.duration._ - val finalAddress = Await.result(wallet.getReceiveAddress(), 40 seconds) - Script.write(addressToPublicKeyScript(finalAddress, chainHash)) - } - - /** NB: this is a blocking call, use carefully! */ - def getWalletPaymentBasepoint(wallet: OnChainAddressGenerator)(implicit ec: ExecutionContext): PublicKey = { - Await.result(wallet.getReceivePubkey(), 40 seconds) - } - def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, commitments: Commitments): RelayFees = { val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees) @@ -276,6 +274,37 @@ object Helpers { object Funding { + /** + * As funder we trust ourselves to not double spend funding txs: we could always use a zero-confirmation watch, + * but we need a scid to send the initial channel_update and remote may not provide an alias. That's why we always + * wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an + * alias in that case). + */ + def minDepthFunder(channelFeatures: ChannelFeatures): Option[Long] = { + if (channelFeatures.hasFeature(Features.ZeroConf)) { + None + } else { + Some(1) + } + } + + /** + * Returns the number of confirmations needed to safely handle the funding transaction, + * we make sure the cumulative block reward largely exceeds the channel size. + * + * @param fundingSatoshis funding amount of the channel + * @return number of confirmations needed, if any + */ + def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match { + case _ if channelFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is + case funding if funding <= Channel.MAX_FUNDING => Some(channelConf.minDepthBlocks) + case funding => + val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 + val scalingFactor = 15 + val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt + Some(channelConf.minDepthBlocks.max(blocksToReachFunding)) + } + def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) @@ -287,20 +316,21 @@ object Helpers { * * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { - val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat - val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat + def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, commitTxFeerate: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { + val toLocalMsat = if (localParams.isInitiator) fundingAmount.toMilliSatoshi - pushMsat else pushMsat + val toRemoteMsat = if (localParams.isInitiator) pushMsat else fundingAmount.toMilliSatoshi - pushMsat - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat) - if (!localParams.isFunder) { - // they are funder, therefore they pay the fee: we need to make sure they can afford it! + if (!localParams.isInitiator) { + // they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocal val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) - val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees + val reserve = localParams.requestedChannelReserve_opt.getOrElse(0 sat) + val missing = toRemoteMsat.truncateToSatoshi - reserve - fees if (missing < Satoshi(0)) { - return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = reserve, fees = fees)) } } @@ -333,7 +363,7 @@ object Helpers { /** * Check whether we are in sync with our peer. */ - def checkSync(keyManager: ChannelKeyManager, d: HasCommitments, remoteChannelReestablish: ChannelReestablish): SyncResult = { + def checkSync(keyManager: ChannelKeyManager, d: PersistentChannelData, remoteChannelReestablish: ChannelReestablish): SyncResult = { // This is done in two steps: // - step 1: we check our local commitment @@ -341,7 +371,7 @@ object Helpers { // step 2 depends on step 1 because we need to preserve ordering between commit_sig and revocation // step 2: we check the remote commitment - def checkRemoteCommit(d: HasCommitments, remoteChannelReestablish: ChannelReestablish, retransmitRevocation_opt: Option[RevokeAndAck]): SyncResult = { + def checkRemoteCommit(d: PersistentChannelData, remoteChannelReestablish: ChannelReestablish, retransmitRevocation_opt: Option[RevokeAndAck]): SyncResult = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if remoteChannelReestablish.nextLocalCommitmentNumber == waitingForRevocation.nextRemoteCommit.index => // we just sent a new commit_sig but they didn't receive it @@ -439,7 +469,7 @@ object Helpers { * * @return true if channel was never open, or got closed immediately, had never any htlcs and local never had a positive balance */ - def nothingAtStake(data: HasCommitments): Boolean = + def nothingAtStake(data: PersistentChannelData): Boolean = data.commitments.localCommit.index == 0 && data.commitments.localCommit.spec.toLocal == 0.msat && data.commitments.remoteCommit.index == 0 && @@ -481,7 +511,7 @@ object Helpers { * because we don't store the closing tx in the channel state * @return the channel closing type, if applicable */ - def isClosed(data: HasCommitments, additionalConfirmedTx_opt: Option[Transaction]): Option[ClosingType] = data match { + def isClosed(data: PersistentChannelData, additionalConfirmedTx_opt: Option[Transaction]): Option[ClosingType] = data match { case closing: DATA_CLOSING if additionalConfirmedTx_opt.exists(closing.mutualClosePublished.map(_.tx).contains) => val closingTx = closing.mutualClosePublished.find(_.tx.txid == additionalConfirmedTx_opt.get.txid).get Some(MutualClose(closingTx)) @@ -499,108 +529,111 @@ object Helpers { case _ => None } - // used only to compute tx weights and estimate fees - lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey + object MutualClose { - def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { - Try(Script.parse(scriptPubKey)) match { - case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true - case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true - case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true - case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true - case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if allowAnySegwit && 2 <= program.length && program.length <= 40 => true - case _ => false + // used only to compute tx weights and estimate fees + lazy val dummyPublicKey: PublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey + + def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { + Try(Script.parse(scriptPubKey)) match { + case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true + case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true + case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true + case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true + case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if allowAnySegwit && 2 <= program.length && program.length <= 40 => true + case _ => false + } } - } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { - import commitments._ - // this is just to estimate the weight, it depends on size of the pubkey scripts - val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey - val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey - val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) - val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) - log.info(s"using feerates=$feerates for initial closing tx") - feerates.computeFees(closingWeight) - } + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { + import commitments._ + // this is just to estimate the weight, it depends on size of the pubkey scripts + val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey + val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey + val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isInitiator, Satoshi(0), Satoshi(0), localCommit.spec) + val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) + log.info(s"using feerates=$feerates for initial closing tx") + feerates.computeFees(closingWeight) + } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { - val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) - val preferredFeerate = commitments.commitmentFormat match { - case DefaultCommitmentFormat => - // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) - case _: AnchorOutputsCommitmentFormat => requestedFeerate + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { + val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) + val preferredFeerate = commitments.commitmentFormat match { + case DefaultCommitmentFormat => + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) + case _: AnchorOutputsCommitmentFormat => requestedFeerate + } + // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can + // always use CPFP to speed up confirmation if necessary. + val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(feeEstimator.getFeeratePerKw(1008)), preferredFeerate * 2) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) } - // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can - // always use CPFP to speed up confirmation if necessary. - val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(feeEstimator.getFeeratePerKw(1008)), preferredFeerate * 2) - firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) - } - def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 + def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - val closingFees = closingFeerates_opt match { - case Some(closingFeerates) => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) - case None => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) + def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFees = closingFeerates_opt match { + case Some(closingFeerates) => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) + case None => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) + } + makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees) } - makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees) - } - def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - import commitments._ - val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey - val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey - val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) - require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey") - require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey") - log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) - val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimit, closingFees.preferred, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) - val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) - log.info(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") - log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") - (closingTx, closingSigned) - } + def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + import commitments._ + val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey + val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey + val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) + require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey") + require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey") + log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) + val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) + val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isInitiator, dustLimit, closingFees.preferred, localCommit.spec) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) + val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + log.info(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") + log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") + (closingTx, closingSigned) + } - def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { - import commitments._ - val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum - if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat == DefaultCommitmentFormat) { - log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") - Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) - } else { - val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) - if (checkClosingDustAmounts(closingTx)) { - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) - Transactions.checkSpendable(signedClosingTx) match { - case Success(_) => Right(signedClosingTx, closingSigned) - case _ => Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx)) - } + def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { + import commitments._ + val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum + if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat == DefaultCommitmentFormat) { + log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") + Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) } else { - Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx)) + val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) + if (checkClosingDustAmounts(closingTx)) { + val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + Transactions.checkSpendable(signedClosingTx) match { + case Success(_) => Right(signedClosingTx, closingSigned) + case _ => Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx)) + } + } else { + Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx)) + } } } - } - /** - * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk - * that the closing transaction will not be relayed to miners' mempool and will not confirm. - * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits - */ - def checkClosingDustAmounts(closingTx: ClosingTx): Boolean = { - closingTx.tx.txOut.forall(txOut => { - Try(Script.parse(txOut.publicKeyScript)) match { - case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 546.sat - case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => txOut.amount >= 540.sat - case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 294.sat - case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => txOut.amount >= 330.sat - case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => txOut.amount >= 354.sat - case _ => txOut.amount >= 546.sat - } - }) + /** + * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk + * that the closing transaction will not be relayed to miners' mempool and will not confirm. + * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits + */ + def checkClosingDustAmounts(closingTx: ClosingTx): Boolean = { + closingTx.tx.txOut.forall(txOut => { + Try(Script.parse(txOut.publicKeyScript)) match { + case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 546.sat + case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => txOut.amount >= 540.sat + case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 294.sat + case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => txOut.amount >= 330.sat + case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => txOut.amount >= 354.sat + case _ => txOut.amount >= 546.sat + } + }) + } } /** Wraps transaction generation in a Try and filters failures to avoid one transaction negatively impacting a whole commitment. */ @@ -621,355 +654,294 @@ object Helpers { } /** Compute the fee paid by a commitment transaction. */ - def commitTxFee(commitInput: InputInfo, commitTx: Transaction, isFunder: Boolean): Satoshi = { + def commitTxFee(commitInput: InputInfo, commitTx: Transaction, isInitiator: Boolean): Satoshi = { require(commitTx.txIn.size == 1, "transaction must have only one input") require(commitTx.txIn.exists(txIn => txIn.outPoint == commitInput.outPoint), "transaction must spend the funding output") - if (isFunder) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat + if (isInitiator) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat } - /** - * Claim all the HTLCs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions. - * - * @param commitments our commitment data, which include payment preimages - * @return a list of transactions (one per output of the commit tx that we can claim) - */ - def claimCurrentLocalCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { - import commitments._ - require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) - val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - - // first we will claim our main output as soon as the delay is over - val mainDelayedTx = withTxGenerationLog("local-main-delayed") { - Transactions.makeClaimLocalDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimDelayed, sig) - }) - } - - // those are the preimages to existing received htlcs - val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + object LocalClose { - val htlcTxs: Map[OutPoint, Option[HtlcTx]] = localCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _, _), remoteSig) => - if (preimages.contains(paymentHash)) { - // incoming htlc for which we have the preimage: we can spend it immediately - txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, preimages(paymentHash), commitmentFormat)) - } - } else { - // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the - // preimage later, otherwise it will eventually timeout and they will get their funds back - txInfo.input.outPoint -> None - } - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => - // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout - txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat)) - } - }.toMap - - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) - } - ).flatten - - LocalCommitPublished( - commitTx = tx, - claimMainDelayedOutputTx = mainDelayedTx, - htlcTxs = htlcTxs, - claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed - claimAnchorTxs = claimAnchorTxs, - irrevocablySpent = Map.empty) - } - - /** - * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op. - * - * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not - * doing that because it introduces a lot of subtle edge cases. - */ - def claimLocalCommitHtlcTxOutput(localCommitPublished: LocalCommitPublished, keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[TransactionWithInputInfo]) = { - import commitments._ - if (isHtlcSuccess(tx, localCommitPublished) || isHtlcTimeout(tx, localCommitPublished)) { - val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + /** + * Claim all the HTLCs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions. + * + * @param commitments our commitment data, which include payment preimages + * @return a list of transactions (one per output of the commit tx that we can claim) + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): LocalCommitPublished = { + import commitments._ + require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val htlcDelayedTx = withTxGenerationLog("htlc-delayed") { - Transactions.makeHtlcDelayedTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { + val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val feeratePerKwDelayed = onChainFeeConf.feeEstimator.getFeeratePerKw(onChainFeeConf.feeTargets.claimMainBlockTarget) + + // first we will claim our main output as soon as the delay is over + val mainDelayedTx = withTxGenerationLog("local-main-delayed") { + Transactions.makeClaimLocalDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) } - val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx.toSeq) - (localCommitPublished1, htlcDelayedTx) - } else { - (localCommitPublished, None) + + val htlcTxs: Map[OutPoint, Option[HtlcTx]] = claimHtlcOutputs(keyManager, commitments) + + val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) { + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget) + List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + } + ).flatten + } else { + Nil + } + + LocalCommitPublished( + commitTx = tx, + claimMainDelayedOutputTx = mainDelayedTx, + htlcTxs = htlcTxs, + claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed + claimAnchorTxs = claimAnchorTxs, + irrevocablySpent = Map.empty) } - } - /** - * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey - * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. - * - * @param commitments our commitment data, which include payment preimages - * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) - * @param tx the remote commitment transaction that has just been published - * @return a list of transactions (one per output of the commit tx that we can claim) - */ - def claimRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") - val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, commitments.channelConfig, commitments.channelFeatures, remoteCommit.index, commitments.localParams, commitments.remoteParams, commitments.commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) - require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) - val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(commitments.remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!commitments.localParams.isFunder, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) - - // we need to use a rather high fee for htlc-claim because we compete with the counterparty - val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) - - // those are the preimages to existing received htlcs - val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap - - // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa - val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = remoteCommit.spec.htlcs.collect { - case OutgoingHtlc(add: UpdateAddHtlc) => - // NB: we first generate the tx skeleton and finalize it below if we have the preimage, so we set logSuccess to false to avoid logging twice - withTxGenerationLog("claim-htlc-success", logSuccess = false) { - Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) - }.map(claimHtlcTx => { - if (preimages.contains(add.paymentHash)) { + /** + * Claim the output of a local commit tx corresponding to HTLCs. + */ + def claimHtlcOutputs(keyManager: ChannelKeyManager, commitments: Commitments)(implicit log: LoggingAdapter): Map[OutPoint, Option[HtlcTx]] = { + import commitments._ + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + + // those are the preimages to existing received htlcs + val hash2Preimage: Map[ByteVector32, ByteVector32] = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + val failedIncomingHtlcs: Set[Long] = commitments.localChanges.all.collect { + case u: UpdateFailHtlc => u.id + case u: UpdateFailMalformedHtlc => u.id + }.toSet + + localCommit.htlcTxsAndRemoteSigs.collect { + case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _, _), remoteSig) => + if (hash2Preimage.contains(paymentHash)) { // incoming htlc for which we have the preimage: we can spend it immediately - claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Right(Transactions.addSigs(claimHtlcTx, sig, preimages(add.paymentHash))) - } + Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Right(Transactions.addSigs(txInfo, localSig, remoteSig, hash2Preimage(paymentHash), commitmentFormat)) + }) + } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { + // incoming htlc that we know for sure will never be fulfilled downstream: we can safely discard it + None } else { // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the // preimage later, otherwise it will eventually timeout and they will get their funds back - claimHtlcTx.input.outPoint -> None + Some(txInfo.input.outPoint -> None) } - }) - case IncomingHtlc(add: UpdateAddHtlc) => - // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout - // NB: we first generate the tx skeleton and finalize it below, so we set logSuccess to false to avoid logging twice - withTxGenerationLog("claim-htlc-timeout", logSuccess = false) { - Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) - }.map(claimHtlcTx => { - claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Right(Transactions.addSigs(claimHtlcTx, sig)) + case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => + // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout + Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat)) + }) + }.flatten.toMap + } + + /** + * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op. + * + * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not + * doing that because it introduces a lot of subtle edge cases. + */ + def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[HtlcDelayedTx]) = { + import commitments._ + if (isHtlcSuccess(tx, localCommitPublished) || isHtlcTimeout(tx, localCommitPublished)) { + val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) + val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + val htlcDelayedTx = withTxGenerationLog("htlc-delayed") { + Transactions.makeHtlcDelayedTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimDelayed, sig) + }) + } + val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx.toSeq) + (localCommitPublished1, htlcDelayedTx) + } else { + (localCommitPublished, None) + } + } + } + + object RemoteClose { + + /** + * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey + * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. + * + * @param commitments our commitment data, which include payment preimages + * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) + * @param tx the remote commitment transaction that has just been published + * @return a list of transactions (one per output of the commit tx that we can claim) + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): RemoteCommitPublished = { + require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") + + val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, onChainFeeConf.feeEstimator) + + val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) { + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget) + val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) } - }) - }.toSeq.flatten.toMap - - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + ).flatten + } else { + Nil } - ).flatten - if (commitments.channelFeatures.paysDirectlyToWallet) { RemoteCommitPublished( commitTx = tx, - claimMainOutputTx = None, + claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, onChainFeeConf.feeEstimator, onChainFeeConf.feeTargets), claimHtlcTxs = htlcTxs, claimAnchorTxs = claimAnchorTxs, irrevocablySpent = Map.empty ) - } else { - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( - claimHtlcTxs = htlcTxs, - claimAnchorTxs = claimAnchorTxs, - ) - } - } - - /** - * Claim our main output only (not necessary if option_static_remotekey was negotiated). - * - * @param commitments either our current commitment data in case of usual remote uncooperative closing - * or our outdated commitment data in case of data loss protection procedure; in any case it is used only - * to get some constant parameters, not commitment data - * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment - * @param tx the remote commitment transaction that has just been published - * @return a transaction claiming our main output - */ - def claimRemoteCommitMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey - val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - - val mainTx = commitments.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { - Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, localPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) - } } - RemoteCommitPublished( - commitTx = tx, - claimMainOutputTx = mainTx, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, - irrevocablySpent = Map.empty - ) - } - - /** - * When an unexpected transaction spending the funding tx is detected: - * 1) we find out if the published transaction is one of remote's revoked txs - * 2) and then: - * a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds - * b) otherwise there is nothing we can do - * - * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment - */ - def claimRevokedRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { - import commitments._ - require(commitTx.txIn.size == 1, "commitment tx should have 1 input") - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) - val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) - require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") - log.warning(s"a revoked commit has been published with txnumber=$txNumber") - // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain - remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) - .map(d => PrivateKey(d)) - .map(remotePerCommitmentSecret => { - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - + /** + * Claim our main output only + * + * @param commitments either our current commitment data in case of usual remote uncooperative closing + * or our outdated commitment data in case of data loss protection procedure; in any case it is used only + * to get some constant parameters, not commitment data + * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment + * @param tx the remote commitment transaction that has just been published + * @return an optional [[ClaimRemoteCommitMainOutputTx]] transaction claiming our main output + */ + def claimMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = { + if (commitments.channelFeatures.paysDirectlyToWallet) { + // the commitment tx sends funds directly to our wallet, no claim tx needed + None + } else { + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) - // first we will claim our main output right away - val mainTx = channelFeatures match { - case ct if ct.paysDirectlyToWallet => - log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") - None - case ct => ct.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) - } + commitments.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { + Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) } } + } + } - // then we punish them by stealing their main output - val mainPenaltyTx = withTxGenerationLog("main-penalty") { - Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(txinfo, sig) + /** + * Claim our htlc outputs only + */ + def claimHtlcOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): Map[OutPoint, Option[ClaimHtlcTx]] = { + val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, commitments.channelConfig, commitments.channelFeatures, remoteCommit.index, commitments.localParams, commitments.remoteParams, commitments.commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) + require(remoteCommitTx.tx.txid == remoteCommit.txid, "txid mismatch, cannot recompute the current remote commit tx") + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val remoteHtlcPubkey = Generators.derivePubKey(commitments.remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val outputs = makeCommitTxOutputs(!commitments.localParams.isInitiator, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) + // we need to use a rather high fee for htlc-claim because we compete with the counterparty + val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) + + // those are the preimages to existing received htlcs + val hash2Preimage: Map[ByteVector32, ByteVector32] = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + val failedIncomingHtlcs: Set[Long] = commitments.localChanges.all.collect { + case u: UpdateFailHtlc => u.id + case u: UpdateFailMalformedHtlc => u.id + }.toSet + + // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa + remoteCommit.spec.htlcs.collect { + case OutgoingHtlc(add: UpdateAddHtlc) => + // NB: we first generate the tx skeleton and finalize it below if we have the preimage, so we set logSuccess to false to avoid logging twice + withTxGenerationLog("claim-htlc-success", logSuccess = false) { + Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) + }.map(claimHtlcTx => { + if (hash2Preimage.contains(add.paymentHash)) { + // incoming htlc for which we have the preimage: we can spend it immediately + Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Right(Transactions.addSigs(claimHtlcTx, sig, hash2Preimage(add.paymentHash))) + }) + } else if (failedIncomingHtlcs.contains(add.id)) { + // incoming htlc that we know for sure will never be fulfilled downstream: we can safely discard it + None + } else { + // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the + // preimage later, otherwise it will eventually timeout and they will get their funds back + Some(claimHtlcTx.input.outPoint -> None) + } }) - } - - // we retrieve the information needed to rebuild htlc scripts - val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) - log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") - val htlcsRedeemScripts = ( - htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ - htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } - ) - .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) - .toMap - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - withTxGenerationLog("htlc-penalty") { - Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + case IncomingHtlc(add: UpdateAddHtlc) => + // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout + // NB: we first generate the tx skeleton and finalize it below, so we set logSuccess to false to avoid logging twice + withTxGenerationLog("claim-htlc-timeout", logSuccess = false) { + Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) + }.map(claimHtlcTx => { + Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Right(Transactions.addSigs(claimHtlcTx, sig)) }) - } - }.toList.flatten - - RevokedCommitPublished( - commitTx = commitTx, - claimMainOutputTx = mainTx, - mainPenaltyTx = mainPenaltyTx, - htlcPenaltyTxs = htlcPenaltyTxs, - claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx - irrevocablySpent = Map.empty - ) - }) + }) + }.toSeq.flatten.flatten.toMap + } } - /** - * Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key. - * - * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: - * - by spending the corresponding output of the commitment tx, using [[HtlcPenaltyTx]] that we generate as soon as we detect that a revoked commit - * as been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty. - * - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by - * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", - * this is the purpose of this method. - * - * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same - * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. - */ - def claimRevokedHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { - val isHtlcTx = htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && - htlcTx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess.orElse(Scripts.extractPaymentHashFromHtlcTimeout)).nonEmpty - if (isHtlcTx) { - log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}") - // Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key + object RevokedClose { + + /** + * When an unexpected transaction spending the funding tx is detected: + * 1) we find out if the published transaction is one of remote's revoked txs + * 2) and then: + * a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds + * b) otherwise there is nothing we can do + * + * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ - val commitTx = revokedCommitPublished.commitTx - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) + require(commitTx.txIn.size == 1, "commitment tx should have 1 input") val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isInitiator, remoteParams.paymentBasepoint, localPaymentPoint) + require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") + log.warning(s"a revoked commit has been published with txnumber=$txNumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) .map(d => PrivateKey(d)) @@ -977,26 +949,129 @@ object Helpers { val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) - - val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { - withTxGenerationLog("htlc-delayed-penalty") { - claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) - // we need to make sure that the tx is indeed valid - Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - signedTx + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) + + // first we will claim our main output right away + val mainTx = channelFeatures match { + case ct if ct.paysDirectlyToWallet => + log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") + None + case ct => ct.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } + } + } + + // then we punish them by stealing their main output + val mainPenaltyTx = withTxGenerationLog("main-penalty") { + Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(txinfo, sig) + }) + } + + // we retrieve the information needed to rebuild htlc scripts + val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) + log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") + val htlcsRedeemScripts = ( + htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ + htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } + ) + .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) + .toMap + + // and finally we steal the htlc outputs + val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) + withTxGenerationLog("htlc-penalty") { + Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) } - }) - val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) - (revokedCommitPublished1, penaltyTxs) - }).getOrElse((revokedCommitPublished, Nil)) - } else { - (revokedCommitPublished, Nil) + }.toList.flatten + + RevokedCommitPublished( + commitTx = commitTx, + claimMainOutputTx = mainTx, + mainPenaltyTx = mainPenaltyTx, + htlcPenaltyTxs = htlcPenaltyTxs, + claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx + irrevocablySpent = Map.empty + ) + }) + } + + /** + * Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key. + * + * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: + * - by spending the corresponding output of the commitment tx, using [[HtlcPenaltyTx]] that we generate as soon as we detect that a revoked commit + * as been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty. + * - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by + * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", + * this is the purpose of this method. + * + * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same + * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. + */ + def claimHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { + val isHtlcTx = htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && + htlcTx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess.orElse(Scripts.extractPaymentHashFromHtlcTimeout)).nonEmpty + if (isHtlcTx) { + log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}") + // Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key + import commitments._ + val commitTx = revokedCommitPublished.commitTx + val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + // this tx has been published by remote, so we need to invert local/remote params + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isInitiator, remoteParams.paymentBasepoint, localPaymentPoint) + // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain + remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) + .map(d => PrivateKey(d)) + .map(remotePerCommitmentSecret => { + val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey + val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + + // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) + + val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { + withTxGenerationLog("htlc-delayed-penalty") { + claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) + // we need to make sure that the tx is indeed valid + Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + signedTx + }) + } + }) + val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) + (revokedCommitPublished1, penaltyTxs) + }).getOrElse((revokedCommitPublished, Nil)) + } else { + (revokedCommitPublished, Nil) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index c3b465c840..5dad716c18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -16,44 +16,60 @@ package fr.acinq.eclair.channel -import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId +import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.Register._ +import fr.acinq.eclair.{SubscriptionsComplete, ShortChannelId} /** * Created by PM on 26/01/2016. */ -class Register extends Actor with ActorLogging { +class Register() extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[ChannelCreated]) context.system.eventStream.subscribe(self, classOf[AbstractChannelRestored]) context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) + + // @formatter:off + private case class ChannelTerminated(channel: ActorRef, channelId: ByteVector32) + // @formatter:on override def receive: Receive = main(Map.empty, Map.empty, Map.empty) def main(channels: Map[ByteVector32, ActorRef], shortIds: Map[ShortChannelId, ByteVector32], channelsTo: Map[ByteVector32, PublicKey]): Receive = { case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId, _, _) => - context.watch(channel) + context.watchWith(channel, ChannelTerminated(channel, temporaryChannelId)) context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId)) case event: AbstractChannelRestored => - context.watch(event.channel) + context.watchWith(event.channel, ChannelTerminated(event.channel, event.channelId)) context become main(channels + (event.channelId -> event.channel), shortIds, channelsTo + (event.channelId -> event.remoteNodeId)) case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) => + context.unwatch(channel) + context.watchWith(channel, ChannelTerminated(channel, channelId)) context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId) - case ShortChannelIdAssigned(_, channelId, shortChannelId, _) => - context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo) + case scidAssigned: ShortChannelIdAssigned => + // We map all known scids (real or alias) to the channel_id. The relayer is in charge of deciding whether a real + // scid can be used or not for routing (see option_scid_alias), but the register is neutral. + val m = (scidAssigned.shortIds.real.toOption.toSeq :+ scidAssigned.shortIds.localAlias).map(_ -> scidAssigned.channelId).toMap + // duplicate check for aliases (we use a random value in a large enough space that there should never be collisions) + shortIds.get(scidAssigned.shortIds.localAlias) match { + case Some(channelId) if channelId != scidAssigned.channelId => + log.error("duplicate alias={} for channelIds={},{} this should never happen!", scidAssigned.shortIds.localAlias, channelId, scidAssigned.channelId) + case _ => () + } + context become main(channels, shortIds ++ m, channelsTo) - case Terminated(actor) if channels.values.toSet.contains(actor) => - val channelId = channels.find(_._2 == actor).get._1 - val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(ShortChannelId(0L)) - context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId) + case ChannelTerminated(_, channelId) => + val shortChannelIds = shortIds.collect { case (key, value) if value == channelId => key } + context become main(channels - channelId, shortIds -- shortChannelIds, channelsTo - channelId) case Symbol("channels") => sender() ! channels @@ -63,7 +79,7 @@ class Register extends Actor with ActorLogging { case fwd@Forward(replyTo, channelId, msg) => // for backward compatibility with legacy ask, we use the replyTo as sender - val compatReplyTo = if (replyTo == ActorRef.noSender) sender() else replyTo + val compatReplyTo = if (replyTo == null) sender() else replyTo.toClassic channels.get(channelId) match { case Some(channel) => channel.tell(msg, compatReplyTo) case None => compatReplyTo ! ForwardFailure(fwd) @@ -71,7 +87,7 @@ class Register extends Actor with ActorLogging { case fwd@ForwardShortId(replyTo, shortChannelId, msg) => // for backward compatibility with legacy ask, we use the replyTo as sender - val compatReplyTo = if (replyTo == ActorRef.noSender) sender() else replyTo + val compatReplyTo = if (replyTo == null) sender() else replyTo.toClassic shortIds.get(shortChannelId).flatMap(channels.get) match { case Some(channel) => channel.tell(msg, compatReplyTo) case None => compatReplyTo ! ForwardShortIdFailure(fwd) @@ -81,9 +97,11 @@ class Register extends Actor with ActorLogging { object Register { + def props(): Props = Props(new Register()) + // @formatter:off - case class Forward[T](replyTo: ActorRef, channelId: ByteVector32, message: T) - case class ForwardShortId[T](replyTo: ActorRef, shortChannelId: ShortChannelId, message: T) + case class Forward[T](replyTo: akka.actor.typed.ActorRef[ForwardFailure[T]], channelId: ByteVector32, message: T) + case class ForwardShortId[T](replyTo: akka.actor.typed.ActorRef[ForwardShortIdFailure[T]], shortChannelId: ShortChannelId, message: T) case class ForwardFailure[T](fwd: Forward[T]) case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala similarity index 55% rename from eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 118422c5ed..9decc58b0d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -14,17 +14,15 @@ * limitations under the License. */ -package fr.acinq.eclair.channel +package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, TypedActorRefOps, actorRefAdapter} -import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, Status, SupervisorStrategy, typed} +import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} +import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed} import akka.event.Logging.MDC -import akka.pattern.pipe -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain._ @@ -33,29 +31,28 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult -import fr.acinq.eclair.channel.Helpers.{Closing, Funding, Syncing, getRelayFees} +import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees, scidForChannelUpdate} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} -import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer -import fr.acinq.eclair.payment.PaymentSettlingOnChain import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{ClosingTx, TxOwner} +import fr.acinq.eclair.transactions.Transactions.ClosingTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector -import java.sql.SQLException import scala.collection.immutable.Queue import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import scala.util.{Failure, Random, Success, Try} +import scala.util.Random /** * Created by PM on 20/08/2015. @@ -63,6 +60,30 @@ import scala.util.{Failure, Random, Success, Try} object Channel { + case class ChannelConf(channelFlags: ChannelFlags, + dustLimit: Satoshi, + maxRemoteDustLimit: Satoshi, + htlcMinimum: MilliSatoshi, + maxHtlcValueInFlightMsat: UInt64, + maxAcceptedHtlcs: Int, + reserveToFundingRatio: Double, + maxReserveToFundingRatio: Double, + minFundingPublicSatoshis: Satoshi, + minFundingPrivateSatoshis: Satoshi, + maxFundingSatoshis: Satoshi, + toRemoteDelay: CltvExpiryDelta, + maxToLocalDelay: CltvExpiryDelta, + minDepthBlocks: Int, + expiryDelta: CltvExpiryDelta, + fulfillSafetyBeforeTimeout: CltvExpiryDelta, + minFinalExpiryDelta: CltvExpiryDelta, + maxBlockProcessingDelay: FiniteDuration, + maxTxPublishRetryDelay: FiniteDuration, + unhandledExceptionStrategy: UnhandledExceptionStrategy, + revocationTimeout: FiniteDuration) { + def minFundingSatoshis(announceChannel: Boolean): Satoshi = if (announceChannel) minFundingPublicSatoshis else minFundingPrivateSatoshis + } + trait TxPublisherFactory { def spawnTxPublisher(context: ActorContext, remoteNodeId: PublicKey): typed.ActorRef[TxPublisher.Command] } @@ -93,14 +114,13 @@ object Channel { // we won't exchange more than this many signatures when negotiating the closing fee val MAX_NEGOTIATION_ITERATIONS = 20 - // this is defined in BOLT 11 - val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) + val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_CLTV_EXPIRY_DELTA val MAX_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(7 * 144) // one week // since BOLT 1.1, there is a max value for the refund delay of the main commitment tx val MAX_TO_SELF_DELAY: CltvExpiryDelta = CltvExpiryDelta(2016) - // as a fundee, we will wait that many blocks for the funding tx to confirm (funder will rely on the funding tx being double-spent) + // as a non-initiator, we will wait that many blocks for the funding tx to confirm (initiator will rely on the funding tx being double-spent) val FUNDING_TIMEOUT_FUNDEE = 2016 // pruning occurs if no new update has been received in two weeks (BOLT 7) @@ -140,31 +160,37 @@ object Channel { } -class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remoteNodeId: PublicKey, blockchain: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, txPublisherFactory: Channel.TxPublisherFactory, origin_opt: Option[ActorRef] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends FSM[ChannelState, ChannelData] with FSMDiagnosticActorLogging[ChannelState, ChannelData] { +class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val remoteNodeId: PublicKey, val blockchain: typed.ActorRef[ZmqWatcher.Command], val relayer: ActorRef, val txPublisherFactory: Channel.TxPublisherFactory, val origin_opt: Option[ActorRef] = None)(implicit val ec: ExecutionContext = ExecutionContext.Implicits.global) + extends FSM[ChannelState, ChannelData] + with FSMDiagnosticActorLogging[ChannelState, ChannelData] + with ChannelOpenSingleFunder + with CommonHandlers + with FundingHandlers + with ErrorHandlers { import Channel._ - private val keyManager: ChannelKeyManager = nodeParams.channelKeyManager + val keyManager: ChannelKeyManager = nodeParams.channelKeyManager // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog // we assume that the peer is the channel's parent - private val peer = context.parent + val peer = context.parent // noinspection ActorMutableStateInspection // the last active connection we are aware of; note that the peer manages connections and asynchronously notifies // the channel, which means that if we get disconnected, the previous active connection will die and some messages will // be sent to dead letters, before the channel gets notified of the disconnection; knowing that this will happen, we // choose to not make this an Option (that would be None before the first connection), and instead embrace the fact // that the active connection may point to dead letters at all time - private var activeConnection = context.system.deadLetters + var activeConnection = context.system.deadLetters - private val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId) + val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId) // this will be used to detect htlc timeouts context.system.eventStream.subscribe(self, classOf[CurrentBlockHeight]) // the constant delay by which we delay processing of blocks (it will be smoothened among all channels) - private val blockProcessingDelay = Random.nextLong(nodeParams.maxBlockProcessingDelay.toMillis + 1).millis + private val blockProcessingDelay = Random.nextLong(nodeParams.channelConf.maxBlockProcessingDelay.toMillis + 1).millis // this will be used to make sure the current commitment fee is up-to-date context.system.eventStream.subscribe(self, classOf[CurrentFeerates]) @@ -179,35 +205,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo 8888888 888 Y888 8888888 888 */ - /* - NEW - FUNDER FUNDEE - | | - | open_channel |WAIT_FOR_OPEN_CHANNEL - |------------------------------->| - WAIT_FOR_ACCEPT_CHANNEL| | - | accept_channel | - |<-------------------------------| - | |WAIT_FOR_FUNDING_CREATED - | funding_created | - |------------------------------->| - WAIT_FOR_FUNDING_SIGNED| | - | funding_signed | - |<-------------------------------| - WAIT_FOR_FUNDING_LOCKED| |WAIT_FOR_FUNDING_LOCKED - | funding_locked funding_locked | - |--------------- ---------------| - | \/ | - | /\ | - |<-------------- -------------->| - NORMAL| |NORMAL - */ - startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, commitTxFeerate, Some(fundingTxFeerate))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey @@ -221,9 +223,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo pushMsat = pushMsat, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, + channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), htlcMinimumMsat = localParams.htlcMinimum, - feeratePerKw = initialFeeratePerKw, + feeratePerKw = commitTxFeerate, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubKey, @@ -239,7 +241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo )) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open - case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder => + case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isInitiator => activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, inputFundee.temporaryChannelId) goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee) @@ -254,7 +256,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.info("we have nothing at stake, going straight to CLOSED") goto(CLOSED) using closing case closing: DATA_CLOSING => - val isFunder = closing.commitments.localParams.isFunder + val isInitiator = closing.commitments.localParams.isInitiator // we don't put back the WatchSpent if the commitment tx has already been published and the spending tx already reached mindepth val closingType_opt = Closing.isClosingTypeAlreadyKnown(closing) log.info(s"channel is closing (closingType=${closingType_opt.map(c => EventType.Closed(c).label).getOrElse("UnknownYet")})") @@ -263,7 +265,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // - there is no need to attempt to publish transactions for other type of closes closingType_opt match { case Some(c: Closing.MutualClose) => - doPublish(c.tx, isFunder) + doPublish(c.tx, isInitiator) case Some(c: Closing.LocalClose) => doPublish(c.localCommitPublished, closing.commitments) case Some(c: Closing.RemoteClose) => @@ -275,7 +277,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case None => // in all other cases we need to be ready for any type of closing watchFundingTx(data.commitments, closing.spendingTxs.map(_.txid).toSet) - closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isFunder)) + closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isInitiator)) closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments)) closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments)) closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments)) @@ -297,15 +299,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case normal: DATA_NORMAL => watchFundingTx(data.commitments) - context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) + context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.shortIds, remoteNodeId)) // we check the configuration because the values for channel_update may have changed while eclair was down val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) if (fees.feeBase != normal.channelUpdate.feeBaseMsat || fees.feeProportionalMillionths != normal.channelUpdate.feeProportionalMillionths || - nodeParams.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { + nodeParams.channelConf.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { log.info("refreshing channel_update due to configuration changes") - self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths, Some(nodeParams.expiryDelta)) + self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths, Some(nodeParams.channelConf.expiryDelta)) } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network // we take into account the date of the last update so that we don't send superfluous updates when we restart the app @@ -329,364 +331,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo watchFundingTx(data.commitments) goto(OFFLINE) using data } - - case Event(c: CloseCommand, d) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) - handleFastClose(c, d.channelId) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { - case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => - Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { - case Left(t) => handleLocalError(t, d, Some(open)) - case Right((channelFeatures, remoteShutdownScript)) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) - val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis) - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty - val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, - dustLimitSatoshis = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, - minimumDepth = minimumDepth, - htlcMinimumMsat = localParams.htlcMinimum, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubkey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(channelType) - )) - val remoteParams = RemoteParams( - nodeId = remoteNodeId, - dustLimit = open.dustLimitSatoshis, - maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = open.htlcMinimumMsat, - toSelfDelay = open.toSelfDelay, - maxAcceptedHtlcs = open.maxAcceptedHtlcs, - fundingPubKey = open.fundingPubkey, - revocationBasepoint = open.revocationBasepoint, - paymentBasepoint = open.paymentBasepoint, - delayedPaymentBasepoint = open.delayedPaymentBasepoint, - htlcBasepoint = open.htlcBasepoint, - initFeatures = remoteInit.features, - shutdownScript = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept - } - - case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) - - case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) - }) - - when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => - Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { - case Left(t) => - channelOpenReplyToUser(Left(LocalError(t))) - handleLocalError(t, d, Some(accept)) - case Right((channelFeatures, remoteShutdownScript)) => - val remoteParams = RemoteParams( - nodeId = remoteNodeId, - dustLimit = accept.dustLimitSatoshis, - maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = accept.htlcMinimumMsat, - toSelfDelay = accept.toSelfDelay, - maxAcceptedHtlcs = accept.maxAcceptedHtlcs, - fundingPubKey = accept.fundingPubkey, - revocationBasepoint = accept.revocationBasepoint, - paymentBasepoint = accept.paymentBasepoint, - delayedPaymentBasepoint = accept.delayedPaymentBasepoint, - htlcBasepoint = accept.htlcBasepoint, - initFeatures = remoteInit.features, - shutdownScript = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.lastSent.temporaryChannelId))) - handleFastClose(c, d.lastSent.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => - // let's create the first commitment tx that spends the yet uncommitted funding tx - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) - case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelFeatures.commitmentFormat) - // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxid = fundingTx.hash, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) - val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated - } - - case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - log.error(t, s"wallet returned error: ") - channelOpenReplyToUser(Left(LocalError(t))) - handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) - handleFastClose(c, d.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => - // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) - case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - // check remote signature validity - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) - case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelFeatures.commitmentFormat) - val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) - // watch the funding tx transaction - val commitInput = localCommitTx.input - val fundingSigned = FundingSigned( - channelId = channelId, - signature = localSigOfRemoteTx - ) - val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, - LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), - LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), - localNextHtlcId = 0L, remoteNextHtlcId = 0L, - originChannels = Map.empty, - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, - commitInput, ShaChain.init) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - // NB: we don't send a ChannelSignatureSent for the first commit - log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - watchFundingTx(commitments) - val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned - } - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_CREATED) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) - handleFastClose(c, d.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { - case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => - // we make sure that their sig checks out and that our first commit tx is spendable - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(cause) => - // we rollback the funding tx, it will never be published - wallet.rollback(fundingTx) - channelOpenReplyToUser(Left(LocalError(cause))) - handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) - case Success(_) => - val commitInput = localCommitTx.input - val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, - LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), remoteCommit, - LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), - localNextHtlcId = 0L, remoteNextHtlcId = 0L, - originChannels = Map.empty, - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array - commitInput, ShaChain.init) - val blockHeight = nodeParams.currentBlockHeight - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - watchFundingTx(commitments) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks) - log.info(s"committing txid=${fundingTx.txid}") - - // we will publish the funding tx only after the channel state has been written to disk because we want to - // make sure we first persist the commitment that returns back the funds to us in case of problem - def publishFundingTx(): Unit = { - wallet.commit(fundingTx).onComplete { - case Success(true) => - context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(channelId))) - case Success(false) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) - self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published - case Failure(t) => - channelOpenReplyToUser(Left(LocalError(t))) - log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast - } - } - - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) - handleFastClose(c, d.channelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { - case Event(msg: FundingLocked, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - log.info(s"received their FundingLocked, deferring message") - stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected - - case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => - Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { - case Success(_) => - log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks) - if (!d.commitments.localParams.isFunder) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) - context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) - val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) - deferred.foreach(self ! _) - // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel - // as soon as it reaches NORMAL state, and before it is announced on the network - // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) - val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) - goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked - case Failure(t) => - log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") - goto(CLOSED) - } - - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel => - log.debug("received remote announcement signatures, delaying") - // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) - // note: no need to persist their message, in case of disconnection they will resend it - context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) - stay() - - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) - - case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) - - case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx match { - case Some(_) => stay() // we are funder, we're still waiting for the funding tx to be confirmed - case None if c.blockHeight - d.waitingSince > FUNDING_TIMEOUT_FUNDEE => - log.warning(s"funding tx hasn't been published in ${c.blockHeight - d.waitingSince} blocks") - self ! BITCOIN_FUNDING_TIMEOUT - stay() - case None => stay() // let's wait longer - } - - case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) - }) - - when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { - case Event(FundingLocked(_, nextPerCommitmentPoint, _), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => - // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) - // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val fees = getRelayFees(nodeParams, remoteNodeId, commitments) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network - context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() - - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => - log.debug("received remote announcement signatures, delaying") - // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) - // note: no need to persist their message, in case of disconnection they will resend it - context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) - stay() - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) }) /* @@ -711,7 +355,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendAdd(d.commitments, c, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) match { case Right((commitments1, add)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate)) } @@ -726,7 +370,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendFulfill(d.commitments, c) match { case Right((commitments1, fulfill)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fulfill case Left(cause) => // we acknowledge the command right away in case of failure @@ -746,7 +390,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendFail(d.commitments, c, nodeParams.privateKey) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -757,7 +401,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendFailMalformed(d.commitments, c) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -780,7 +424,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendFee(d.commitments, c, nodeParams.onChainFeeConf) match { case Right((commitments1, fee)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fee case Left(cause) => handleCommandError(cause, c) } @@ -800,7 +444,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendCommit(d.commitments, keyManager) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", Commitments.specs2String(commitments1)) - val nextRemoteCommit = commitments1.remoteNextCommitInfo.swap.toOption.get.nextRemoteCommit + val nextRemoteCommit = commitments1.nextRemoteCommit_opt.get val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter @@ -817,7 +461,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) // we expect a quick response from our peer - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) handleCommandSuccess(c, d.copy(commitments = commitments1)).storing().sending(commit).acking(commitments1.localChanges.signed) case Left(cause) => handleCommandError(cause, c) } @@ -837,7 +481,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) { // we send this event only when our balance changes - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) } context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) stay() using d.copy(commitments = commitments1) storing() sending revocation @@ -950,12 +594,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // are there pending signed changes on either side? we need to have received their last revocation! if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { // there are no pending signed changes, let's go directly to NEGOTIATING - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending sendList } } else { @@ -969,62 +613,76 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(c: CurrentFeerates, d: DATA_NORMAL) => handleCurrentFeerate(c, d) - case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, _), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => - val shortChannelId = ShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt) - log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=$shortChannelId") - // if final shortChannelId is different from the one we had before, we need to re-announce it - val channelUpdate = if (shortChannelId != d.shortChannelId) { - log.info(s"short channel id changed, probably due to a chain reorg: old=${d.shortChannelId} new=$shortChannelId") - // we need to re-announce this shortChannelId - context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId, Some(d.shortChannelId))) + case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => + val finalRealShortId = RealScidStatus.Final(RealShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)) + log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=${finalRealShortId.realScid}") + val shortIds1 = d.shortIds.copy(real = finalRealShortId) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId)) + if (d.shortIds.real == RealScidStatus.Unknown) { + // this is a zero-conf channel and it is the first time we know for sure that the funding tx has been confirmed + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) + } + val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1.localAlias) + // if the shortChannelId is different from the one we had before, we need to re-announce it + val channelUpdate1 = if (d.channelUpdate.shortChannelId != scidForChannelUpdate) { + log.info(s"using new scid in channel_update: old=${d.channelUpdate.shortChannelId} new=$scidForChannelUpdate") // we re-announce the channelUpdate for the same reason - Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - } else d.channelUpdate - val localAnnSigs_opt = if (d.commitments.announceChannel) { + Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + } else { + d.channelUpdate + } + if (d.commitments.announceChannel) { // if channel is public we need to send our announcement_signatures in order to generate the channel_announcement - Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)) - } else None - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(shortChannelId = shortChannelId, buried = true, channelUpdate = channelUpdate) storing() sending localAnnSigs_opt.toSeq + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs + } else { + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() + } case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel => // channels are publicly announced if both parties want it (defined as feature bit) - if (d.buried) { - // we are aware that the channel has reached enough confirmations - // we already had sent our announcement_signatures but we don't store them so we need to recompute it - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) - d.channelAnnouncement match { - case None => - require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") - log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") - import d.commitments.{localParams, remoteParams} - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) - if (!Announcements.checkSigs(channelAnn)) { - handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) - } else { - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn)) storing() - } - case Some(_) => - // they have sent their announcement sigs, but we already have a valid channel announcement - // this can happen if our announcement_signatures was lost during a disconnection - // specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message" - // current implementation always replies to announcement_signatures, not only the first time - // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway - log.info("re-sending our announcement sigs") - stay() sending localAnnSigs - } - } else { - // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message - // note: no need to persist their message, in case of disconnection they will resend it - log.debug("received remote announcement signatures, delaying") - context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) - stay() + d.shortIds.real match { + case RealScidStatus.Final(realScid) => + // we are aware that the channel has reached enough confirmations + // we already had sent our announcement_signatures but we don't store them so we need to recompute it + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realScid) + d.channelAnnouncement match { + case None => + require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") + log.info(s"announcing channelId=${d.channelId} on the network with shortId=${localAnnSigs.shortChannelId}") + import d.commitments.{localParams, remoteParams} + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + if (!Announcements.checkSigs(channelAnn)) { + handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) + } else { + // we generate a new channel_update because the scid used may change if we were previously using an alias + val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds.localAlias) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate) storing() + } + case Some(_) => + // they have sent their announcement sigs, but we already have a valid channel announcement + // this can happen if our announcement_signatures was lost during a disconnection + // specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message" + // current implementation always replies to announcement_signatures, not only the first time + // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway + log.info("re-sending our announcement sigs") + stay() sending localAnnSigs + } + case _ => + // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message + // note: no need to persist their message, in case of disconnection they will resend it + log.debug("received remote announcement signatures, delaying") + context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) + stay() } case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -1033,7 +691,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = TimestampSecond.now() - d.channelUpdate.timestamp - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) reason match { case Reconnected if d.commitments.announceChannel && Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => // we already sent an identical channel_update not long ago (flapping protection in case we keep being disconnected/reconnected) @@ -1055,22 +713,21 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // we cancel the timer that would have made us send the enabled update after reconnection (flappy channel protection) cancelTimer(Reconnected.toString) // if we have pending unsigned htlcs, then we cancel them and generate an update with the disabled flag set, that will be returned to the sender in a temporary channel failure - val d1 = if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { + if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.debug("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // NB: the htlcs stay() in the commitments.localChange, they will be cleaned up after reconnection d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) } - d.copy(channelUpdate = channelUpdate1) + goto(OFFLINE) using d.copy(channelUpdate = channelUpdate1) storing() } else { - d + goto(OFFLINE) using d } - goto(OFFLINE) using d1 case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) - case Event(_: FundingLocked, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel + case Event(_: ChannelReady, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel }) @@ -1174,7 +831,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) // we expect a quick response from our peer - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) handleCommandSuccess(c, d.copy(commitments = commitments1)).storing().sending(commit).acking(commitments1.localChanges.signed) case Left(cause) => handleCommandError(cause, c) } @@ -1190,12 +847,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.debug("received a new sig:\n{}", Commitments.specs2String(commitments1)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending revocation } } else { @@ -1230,12 +887,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { log.debug("switching to NEGOTIATING spec:\n{}", Commitments.specs2String(commitments1)) - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() } } else { @@ -1289,7 +946,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(c: ClosingSigned, d: DATA_NEGOTIATING) => log.info("received closing fee={}", c.feeSatoshis) val (remoteClosingFee, remoteSig) = (c.feeSatoshis, c.signature) - Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { + Closing.MutualClose.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { case Right((signedClosingTx, closingSignedRemoteFees)) => val lastLocalClosingSigned_opt = d.closingTxProposed.last.lastOption if (lastLocalClosingSigned_opt.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee)) { @@ -1309,10 +966,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { c.feeRange_opt match { - case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isFunder => - // if we are fundee and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation + case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isInitiator => + // if we are not the channel initiator and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation - val localClosingFees = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) if (maxFee < localClosingFees.min) { log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min) stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}") @@ -1327,7 +984,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.info("accepting their closing fee={}", remoteClosingFee) handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { - val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) + val (closingTx, closingSigned) = Closing.MutualClose.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) log.info("proposing closing fee={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee) val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1338,10 +995,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case _ => val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) val (closingTx, closingSigned) = { - // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val localClosingFees = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextPreferredFee = Closing.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) - Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) + // if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee + val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextPreferredFee = Closing.MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) + Closing.MutualClose.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) } val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1385,7 +1042,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo handleCommandError(ClosingAlreadyInProgress(d.channelId), c) } else { log.info("updating our closing feerates: {}", feerates) - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, Some(feerates)) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, Some(feerates)) val closingTxProposed1 = d.closingTxProposed match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned)) @@ -1401,17 +1058,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo }) when(CLOSING)(handleExceptions { - case Event(c: CMD_FULFILL_HTLC, d: DATA_CLOSING) => - Commitments.sendFulfill(d.commitments, c) match { + case Event(c: HtlcSettlementCommand, d: DATA_CLOSING) => + (c match { + case c: CMD_FULFILL_HTLC => Commitments.sendFulfill(d.commitments, c) + case c: CMD_FAIL_HTLC => Commitments.sendFail(d.commitments, c, nodeParams.privateKey) + case c: CMD_FAIL_MALFORMED_HTLC => Commitments.sendFailMalformed(d.commitments, c) + }) match { case Right((commitments1, _)) => - log.info("got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") - val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) - val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) - val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => { - require(commitments1.remoteNextCommitInfo.isLeft, "next remote commit must be defined") - val remoteCommit = commitments1.remoteNextCommitInfo.swap.toOption.get.nextRemoteCommit - Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, remoteCommit, remoteCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - }) + log.info("got valid settlement for htlc={}, recalculating htlc transactions", c.id) + + val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => localCommitPublished.copy(htlcTxs = Closing.LocalClose.claimHtlcOutputs(keyManager, commitments1))) + val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitments1, commitments1.remoteCommit, nodeParams.onChainFeeConf.feeEstimator))) + val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitments1, commitments1.nextRemoteCommit_opt.get, nodeParams.onChainFeeConf.feeEstimator))) def republish(): Unit = { localCommitPublished1.foreach(lcp => doPublish(lcp, commitments1)) @@ -1467,7 +1125,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(WatchOutputSpentTriggered(tx), d: DATA_CLOSING) => // one of the outputs of the local/remote/revoked commit was spent // we just put a watch to be notified when it is confirmed - blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.minDepthBlocks) + blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks) // when a remote or local commitment tx containing outgoing htlcs is published on the network, // we watch it in order to extract payment preimage if funds are pulled by the counterparty // we can then use these preimages to fulfill origin htlcs @@ -1485,7 +1143,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => - val (rev1, penaltyTxs) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) + val (rev1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None)) penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, hints = Set(claimTx.tx.txid))) rev1 @@ -1499,10 +1157,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val d1 = d.copy( localCommitPublished = d.localCommitPublished.map(localCommitPublished => { // If the tx is one of our HTLC txs, we now publish a 3rd-stage claim-htlc-tx that claims its output. - val (localCommitPublished1, claimHtlcTx_opt) = Closing.claimLocalCommitHtlcTxOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (localCommitPublished1, claimHtlcTx_opt) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) claimHtlcTx_opt.foreach(claimHtlcTx => { txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None) - blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.minDepthBlocks) + blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepthBlocks) }) Closing.updateLocalCommitPublished(localCommitPublished1, tx) }), @@ -1577,10 +1235,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo when(CLOSED)(handleExceptions { case Event(Symbol("shutdown"), _) => stateData match { - case d: HasCommitments => + case d: PersistentChannelData => log.info(s"deleting database record for channelId=${d.channelId}") nodeParams.db.channels.removeChannel(d.channelId) - case _ => + case _: TransientChannelData => // nothing was stored in the DB } log.info("shutting down") stop(FSM.Normal) @@ -1604,7 +1262,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val d1 = Helpers.updateFeatures(d, localInit, remoteInit) goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using d1 sending error - case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: HasCommitments) => + case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: PersistentChannelData) => activeConnection = r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) @@ -1627,9 +1285,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // note: this can only happen if state is NORMAL or SHUTDOWN // -> in NEGOTIATING there are no more htlcs // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway - case Event(ProcessCurrentBlockHeight(c), d: HasCommitments) => handleNewBlock(c, d) + case Event(ProcessCurrentBlockHeight(c), d: PersistentChannelData) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) @@ -1649,34 +1307,41 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => handleRemoteSpentFuture(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) => handleRemoteSpentOther(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) }) when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - val minDepth = if (d.commitments.localParams.isFunder) { - nodeParams.minDepthBlocks + val minDepth_opt = if (d.commitments.localParams.isInitiator) { + Helpers.Funding.minDepthFunder(d.commitments.channelFeatures) } else { - // when we're fundee we scale the min_depth confirmations depending on the funding amount - Helpers.minDepthForFunding(nodeParams, d.commitments.commitInput.txOut.amount) + // when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount + Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount) + } + val minDepth = minDepth_opt.getOrElse { + val defaultMinDepth = nodeParams.channelConf.minDepthBlocks + // If we are in state WAIT_FOR_FUNDING_CONFIRMED, then the computed minDepth should be > 0, otherwise we would + // have skipped this state. Maybe the computation method was changed and eclair was restarted? + log.warning("min_depth should be defined since we're waiting for the funding tx to confirm, using default minDepth={}", defaultMinDepth) + defaultMinDepth.toLong } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) - case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => - log.debug("re-sending fundingLocked") + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + goto(WAIT_FOR_CHANNEL_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => Syncing.checkSync(keyManager, d, channelReestablish) match { @@ -1687,12 +1352,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // normal case, our data is up-to-date if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { - // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit funding_locked, otherwise it MUST NOT - log.debug("re-sending fundingLocked") + // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - sendQueue = sendQueue :+ fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + sendQueue = sendQueue :+ channelReady } // we may need to retransmit updates and/or commit_sig and/or revocation @@ -1704,7 +1369,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo commitments1.remoteNextCommitInfo match { case Left(_) => // we expect them to (re-)send the revocation immediately - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) case _ => () } @@ -1720,24 +1385,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo sendQueue = sendQueue :+ localShutdown } - if (!d.buried) { - // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been - // fired while we were in OFFLINE (if not, the operation is idempotent anyway) - blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - } else { - // channel has been buried enough, should we (re)send our announcement sigs? - d.channelAnnouncement match { - case None if !d.commitments.announceChannel => - // that's a private channel, nothing to do - () - case None => + d.shortIds.real match { + case RealScidStatus.Final(realShortChannelId) => + // should we (re)send our announcement sigs? + if (d.commitments.announceChannel && d.channelAnnouncement.isEmpty) { // BOLT 7: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realShortChannelId) sendQueue = sendQueue :+ localAnnSigs - case Some(_) => - // channel was already announced, nothing to do - () - } + } + case _ => + // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been + // fired while we were in OFFLINE (if not, the operation is idempotent anyway) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) } if (d.commitments.announceChannel) { @@ -1754,7 +1413,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so // we send it (if needed) when reconnected. val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty - if (d.commitments.localParams.isFunder && !shutdownInProgress) { + if (d.commitments.localParams.isInitiator && !shutdownInProgress) { val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None) if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { @@ -1767,6 +1426,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) + case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) + case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => Syncing.checkSync(keyManager, d, channelReestablish) match { case syncFailure: SyncResult.Failure => @@ -1780,11 +1441,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(_: ChannelReestablish, d: DATA_NEGOTIATING) => // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - // negotiation restarts from the beginning, and is initialized by the funder + // negotiation restarts from the beginning, and is initialized by the channel initiator // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them - if (d.commitments.localParams.isFunder) { + if (d.commitments.localParams.isInitiator) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned)) goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil } else { @@ -1794,14 +1455,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send - // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a funding_locked + // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a channel_ready // first and then go silent. This is due to a race condition on their side, so we trigger a reconnection, hoping that // we will eventually receive their channel_reestablish. - case Event(_: FundingLocked, d) => - log.warning("received funding_locked before channel_reestablish (known lnd bug): disconnecting...") + case Event(_: ChannelReady, d) => + log.warning("received channel_ready before channel_reestablish (known lnd bug): disconnecting...") // NB: we use a small delay to ensure we've sent our warning before disconnecting. context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) - stay() sending Warning(d.channelId, "spec violation: you sent funding_locked before channel_reestablish") + stay() sending Warning(d.channelId, "spec violation: you sent channel_ready before channel_reestablish") // This handler is a workaround for an issue in lnd similar to the one above: they sometimes send announcement_signatures // before channel_reestablish, which is a minor spec violation. It doesn't halt the channel, we can simply postpone @@ -1811,9 +1472,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) stay() sending Warning(d.channelId, "spec violation: you sent announcement_signatures before channel_reestablish") - case Event(ProcessCurrentBlockHeight(c), d: HasCommitments) => handleNewBlock(c, d) + case Event(ProcessCurrentBlockHeight(c), d: PersistentChannelData) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) @@ -1829,13 +1490,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) => handleRemoteSpentOther(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) - case Event(e: Error, d: HasCommitments) => handleRemoteError(e, d) + case Event(e: Error, d: PersistentChannelData) => handleRemoteError(e, d) }) when(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)(handleExceptions { @@ -1848,30 +1509,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo when(ERR_INFORMATION_LEAK)(errorStateHandler) - when(ERR_FUNDING_LOST)(errorStateHandler) - whenUnhandled { case Event(INPUT_DISCONNECTED, _) => goto(OFFLINE) - case Event(WatchFundingLostTriggered(_), _) => goto(ERR_FUNDING_LOST) - - case Event(c: CMD_GETSTATE, _) => + case Event(c: CMD_GET_CHANNEL_STATE, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETSTATE(stateName) + replyTo ! RES_GET_CHANNEL_STATE(stateName) stay() - case Event(c: CMD_GETSTATEDATA, _) => + case Event(c: CMD_GET_CHANNEL_DATA, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETSTATEDATA(stateData) + replyTo ! RES_GET_CHANNEL_DATA(stateData) stay() - case Event(c: CMD_GETINFO, _) => + case Event(c: CMD_GET_CHANNEL_INFO, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETINFO(remoteNodeId, stateData.channelId, stateName, stateData) + replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, stateData.channelId, stateName, stateData) stay() - case Event(c: CMD_ADD_HTLC, d: HasCommitments) => + case Event(c: CMD_ADD_HTLC, d: PersistentChannelData) => log.info(s"rejecting htlc request in state=$stateName") val error = ChannelUnavailable(d.channelId) handleAddHtlcCommandError(c, error, None) // we don't provide a channel_update: this will be a permanent channel failure @@ -1880,12 +1537,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(c: CMD_FORCECLOSE, d) => d match { - case data: HasCommitments => + case data: PersistentChannelData => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, data.channelId) val failure = ForcedLocalCommit(data.channelId) handleLocalError(failure, data, Some(c)) - case _ => handleCommandError(CommandUnavailableInThisState(d.channelId, "forceclose", stateName), c) + case _: TransientChannelData => + handleCommandError(CommandUnavailableInThisState(d.channelId, "forceclose", stateName), c) } // In states where we don't explicitly handle this command, we won't broadcast a new channel update immediately, @@ -1919,12 +1577,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event("disconnecting", _) => stay() // funding tx was confirmed in time, let's just ignore this - case Event(BITCOIN_FUNDING_TIMEOUT, _: HasCommitments) => stay() + case Event(BITCOIN_FUNDING_TIMEOUT, _: PersistentChannelData) => stay() // peer doesn't cancel the timer case Event(TickChannelOpenTimeout, _) => stay() - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid => + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid => log.warning(s"processing local commit spent in catch-all handler") spendLocalCurrent(d) } @@ -1934,66 +1592,69 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case state -> nextState => if (state != nextState) { val commitments_opt = nextStateData match { - case hasCommitments: HasCommitments => Some(hasCommitments.commitments) - case _ => None + case d: PersistentChannelData => Some(d.commitments) + case _: TransientChannelData => None } context.system.eventStream.publish(ChannelStateChanged(self, nextStateData.channelId, peer, remoteNodeId, state, nextState, commitments_opt)) } if (nextState == CLOSED) { // channel is closed, scheduling this actor for self destruction - context.system.scheduler.scheduleOnce(10 seconds, self, Symbol("shutdown")) + context.system.scheduler.scheduleOnce(1 minute, self, Symbol("shutdown")) } if (nextState == OFFLINE) { // we can cancel the timer, we are not expecting anything when disconnected cancelTimer(RevocationTimeout.toString) } - // if channel is private, we send the channel_update directly to remote - // they need it "to learn the other end's forwarding parameters" (BOLT 7) - (state, nextState, stateData, nextStateData) match { - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && !d1.buried && d2.buried => - // for a private channel, when the tx was just buried we need to send the channel_update to our peer (even if it didn't change) - send(d2.channelUpdate) - case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d2.buried => - // otherwise if we're coming back online, we rebroadcast the latest channel_update - // this makes sure that if the channel_update was missed, we have a chance to re-send it - send(d2.channelUpdate) - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d1.channelUpdate != d2.channelUpdate && d2.buried => - // otherwise, we only send it when it is different, and tx is already buried - send(d2.channelUpdate) - case _ => () - } - - (state, nextState, stateData, nextStateData) match { - // ORDER MATTERS! - case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, normal: DATA_NORMAL) => - Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) { - log.debug("re-emitting channel_update={} enabled={} ", normal.channelUpdate, normal.channelUpdate.channelFlags.isEnabled) + sealed trait EmitLocalChannelEvent + /* + * This event is for: + * - the router: so it knows about this channel to find routes + * - the relayer: so it learns about the channel real scid/alias and can route to it + * - the peer: so they can "learn the other end's forwarding parameters" (BOLT 7) + */ + case class EmitLocalChannelUpdate(reason: String, d: DATA_NORMAL, sendToPeer: Boolean) extends EmitLocalChannelEvent + /* + * When a channel that could previously be used to relay payments starts closing, we advertise the fact that this + * channel can't be used for payments anymore. If the channel is private we don't really need to tell the + * counterparty because it is already aware that the channel is being closed + */ + case class EmitLocalChannelDown(d: DATA_NORMAL) extends EmitLocalChannelEvent + + // We only send the channel_update directly to the peer if we are connected AND the channel hasn't been announced + val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match { + case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("restore", d, sendToPeer = false)) + case (WAIT_FOR_FUNDING_CONFIRMED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) + case (WAIT_FOR_CHANNEL_READY, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) + case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) + case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("syncing->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) + case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->offline", d2, sendToPeer = false)) + case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("offline->offline", d2, sendToPeer = false)) + case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d)) + case _ => None + } + emitEvent_opt.foreach { + case EmitLocalChannelUpdate(reason, d, sendToPeer) => + log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=$sendToPeer realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no")) + val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) + context.system.eventStream.publish(lcu) + if (sendToPeer) { + send(Helpers.channelUpdateForDirectPeer(nodeParams, d.channelUpdate, d.shortIds)) } - context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, normal.commitments)) - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => - // don't do anything if neither the channel_update nor the channel_announcement didn't change - () - case (WAIT_FOR_FUNDING_LOCKED | NORMAL | OFFLINE | SYNCING, NORMAL | OFFLINE, _, normal: DATA_NORMAL) => - // when we do WAIT_FOR_FUNDING_LOCKED->NORMAL or NORMAL->NORMAL or SYNCING->NORMAL or NORMAL->OFFLINE, we send out the new channel_update (most of the time it will just be to enable/disable the channel) - log.info("emitting channel_update={} enabled={} ", normal.channelUpdate, normal.channelUpdate.channelFlags.isEnabled) - context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, normal.commitments)) - case (_, _, _: DATA_NORMAL, _: DATA_NORMAL) => - // in any other case (e.g. OFFLINE->SYNCING) we do nothing - () - case (_, _, normal: DATA_NORMAL, _) => - // when we finally leave the NORMAL state (or OFFLINE with NORMAL data) to go to SHUTDOWN/NEGOTIATING/CLOSING/ERR*, we advertise the fact that channel can't be used for payments anymore - // if the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed - context.system.eventStream.publish(LocalChannelDown(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId)) - case _ => () + case EmitLocalChannelDown(d) => + log.debug(s"emitting channel down event") + val lcd = LocalChannelDown(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId) + context.system.eventStream.publish(lcd) } + // When we change our channel update parameters (e.g. relay fees), we want to advertise it to other actors. (stateData, nextStateData) match { // NORMAL->NORMAL, NORMAL->OFFLINE, SYNCING->NORMAL case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2) - // WAIT_FOR_FUNDING_LOCKED->NORMAL - case (_: DATA_WAIT_FOR_FUNDING_LOCKED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + // WAIT_FOR_FUNDING_CONFIRMED->NORMAL, WAIT_FOR_CHANNEL_READY->NORMAL + case (_: DATA_WAIT_FOR_FUNDING_CONFIRMED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + case (_: DATA_WAIT_FOR_CHANNEL_READY, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) case _ => () } } @@ -2008,7 +1669,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo /** Check pending settlement commands */ onTransition { case _ -> CLOSING => - PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.asInstanceOf[HasCommitments].channelId) match { + PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.channelId) match { case Nil => log.debug("nothing to replay") case cmds => @@ -2016,7 +1677,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo cmds.foreach(self ! _) // they all have commit = false } case SYNCING -> (NORMAL | SHUTDOWN) => - PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.asInstanceOf[HasCommitments].channelId) match { + PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.channelId) match { case Nil => log.debug("nothing to replay") case cmds => @@ -2048,23 +1709,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo 888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P" */ - /** - * This function is used to return feedback to user at channel opening - */ - private def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { - val m = message match { - case Left(LocalError(t)) => Status.Failure(t) - case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) - case Right(s) => s - } - origin_opt.foreach(_ ! m) - } - - private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { + private def handleCurrentFeerate(c: CurrentFeerates, d: PersistentChannelData) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate - val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) - val shouldClose = !d.commitments.localParams.isFunder && + val shouldUpdateFee = d.commitments.localParams.isInitiator && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) + val shouldClose = !d.commitments.localParams.isInitiator && nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) && d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk if (shouldUpdateFee) { @@ -2084,7 +1733,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo * @param d the channel commtiments * @return */ - private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: HasCommitments) = { + private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: PersistentChannelData) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate // if the network fees are too high we risk to not be able to confirm our current commitment @@ -2104,12 +1753,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } - private def handleFastClose(c: CloseCommand, channelId: ByteVector32) = { - val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_SUCCESS(c, channelId) - goto(CLOSED) - } - private def handleCommandSuccess(c: channel.Command, newData: ChannelData) = { val replyTo_opt = c match { case hasOptionalReplyTo: HasOptionalReplyToCommand => hasOptionalReplyTo.replyTo_opt @@ -2125,7 +1768,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.warning(s"${cause.getMessage} while processing cmd=${c.getClass.getSimpleName} in state=$stateName") val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_ADD_FAILED(c, cause, channelUpdate) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = false)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = false)) stay() } @@ -2136,79 +1779,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case hasReplyTo: HasReplyToCommand => if (hasReplyTo.replyTo == ActorRef.noSender) Some(sender()) else Some(hasReplyTo.replyTo) } replyTo_opt.foreach(replyTo => replyTo ! RES_FAILURE(c, cause)) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = false)) - stay() - } - - private def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { - // TODO: should we wait for an acknowledgment from the watcher? - val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs - blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) - // TODO: implement this? (not needed if we use a reasonable min_depth) - //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) - } - - /** - * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction - * that we made for some reason). If the funding tx has been double spent we can forget about the channel. - */ - private def checkDoubleSpent(fundingTx: Transaction): Unit = { - log.debug(s"checking status of funding tx txid=${fundingTx.txid}") - wallet.doubleSpent(fundingTx).onComplete { - case Success(true) => - log.warning(s"funding tx has been double spent! fundingTxid=${fundingTx.txid} fundingTx=$fundingTx") - self ! BITCOIN_FUNDING_PUBLISH_FAILED - case Success(false) => () - case Failure(t) => log.error(t, s"error while testing status of funding tx fundingTxid=${fundingTx.txid}: ") - } - } - - private def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSince: BlockHeight, fundingTx_opt: Option[Transaction]) = { - import getTxResponse._ - tx_opt match { - case Some(_) => () // the funding tx exists, nothing to do - case None => - fundingTx_opt match { - case Some(fundingTx) => - // if we are funder, we never give up - // we cannot correctly set the fee, but it was correctly set when we initially published the transaction - log.info(s"republishing the funding tx...") - txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, "funding", 0 sat, None) - // we also check if the funding tx has been double-spent - checkDoubleSpent(fundingTx) - context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) - case None if (nodeParams.currentBlockHeight - waitingSince) > FUNDING_TIMEOUT_FUNDEE => - // if we are fundee, we give up after some time - log.warning(s"funding tx hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks") - self ! BITCOIN_FUNDING_TIMEOUT - case None => - // let's wait a little longer - log.info(s"funding tx still hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks, will wait ${FUNDING_TIMEOUT_FUNDEE - (nodeParams.currentBlockHeight - waitingSince)} more blocks...") - context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) - } - } + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = false)) stay() } - private def handleFundingPublishFailed(d: HasCommitments) = { - log.error(s"failed to publish funding tx") - val exc = ChannelFundingError(d.channelId) - val error = Error(d.channelId, exc.getMessage) - // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: - // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error - } - - private def handleFundingTimeout(d: HasCommitments) = { - log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") - val exc = FundingTxTimedout(d.channelId) - val error = Error(d.channelId, exc.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error - } - - private def handleRevocationTimeout(revocationTimeout: RevocationTimeout, d: HasCommitments) = { + private def handleRevocationTimeout(revocationTimeout: RevocationTimeout, d: PersistentChannelData) = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if revocationTimeout.remoteCommitNumber + 1 == waitingForRevocation.nextRemoteCommit.index => log.warning(s"waited for too long for a revocation to remoteCommitNumber=${revocationTimeout.remoteCommitNumber}, disconnecting") @@ -2225,11 +1800,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo if (d.channelUpdate.channelFlags.isEnabled) { // if the channel isn't disabled we generate a new channel_update log.info("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // then we update the state and replay the request self forward c // we use goto() to fire transitions - goto(stateName) using d.copy(channelUpdate = channelUpdate1) + goto(stateName) using d.copy(channelUpdate = channelUpdate1) storing() } else { // channel is already disabled, we reply to the request val error = ChannelUnavailable(d.channelId) @@ -2238,7 +1813,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -2250,7 +1825,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo stay() using d.copy(channelUpdate = channelUpdate1) storing() } - private def handleSyncFailure(channelReestablish: ChannelReestablish, syncFailure: SyncResult.Failure, d: HasCommitments) = { + private def handleSyncFailure(channelReestablish: ChannelReestablish, syncFailure: SyncResult.Failure, d: PersistentChannelData) = { syncFailure match { case res: SyncResult.LocalLateProven => log.error(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourLocalCommitmentNumber=${res.ourLocalCommitmentNumber} theirRemoteCommitmentNumber=${res.theirRemoteCommitmentNumber}") @@ -2275,13 +1850,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo private def maybeEmitChannelUpdateChangedEvent(newUpdate: ChannelUpdate, oldUpdate_opt: Option[ChannelUpdate], d: DATA_NORMAL): Unit = { if (oldUpdate_opt.isEmpty || !Announcements.areSameIgnoreFlags(newUpdate, oldUpdate_opt.get)) { - context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, newUpdate.shortChannelId, d.commitments.remoteNodeId, newUpdate)) + context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, d.commitments.remoteNodeId, newUpdate)) } } - private def handleNewBlock(c: CurrentBlockHeight, d: HasCommitments) = { + private def handleNewBlock(c: CurrentBlockHeight, d: PersistentChannelData) = { val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockHeight) - val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.fulfillSafetyBeforeTimeout) + val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.channelConf.fulfillSafetyBeforeTimeout) if (timedOutOutgoing.nonEmpty) { // Downstream timed out. handleLocalError(HtlcsTimedoutDownstream(d.channelId, timedOutOutgoing), d, Some(c)) @@ -2308,73 +1883,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } - private def handleLocalError(cause: Throwable, d: ChannelData, msg: Option[Any]) = { - cause match { - case _: ForcedLocalCommit => - log.warning(s"force-closing channel at user request") - case _ if msg.exists(_.isInstanceOf[OpenChannel]) || msg.exists(_.isInstanceOf[AcceptChannel]) => - // invalid remote channel parameters are logged as warning - log.warning(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") - case _: ChannelException => - log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") - case _ => - // unhandled error: we dump the channel data, and print the stack trace - log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData:") - } - - val error = Error(d.channelId, cause.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = true)) - - d match { - case dd: HasCommitments if Closing.nothingAtStake(dd) => goto(CLOSED) - case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => - log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}") - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that - handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) - case dd: HasCommitments => - cause match { - case _: ChannelException => - // known channel exception: we force close using our current commitment - spendLocalCurrent(dd) sending error - case _ => - // unhandled exception: we apply the configured strategy - nodeParams.unhandledExceptionStrategy match { - case UnhandledExceptionStrategy.LocalClose => - spendLocalCurrent(dd) sending error - case UnhandledExceptionStrategy.Stop => - log.error("unhandled exception: standard procedure would be to force-close the channel, but eclair has been configured to halt instead.") - NotificationsLogger.logFatalError( - s"""stopping node as configured strategy to unhandled exceptions for nodeId=$remoteNodeId channelId=${d.channelId} - | - |Eclair has been configured to shut down when an unhandled exception happens, instead of requesting a - |force-close from the peer. This gives the operator a chance of avoiding an unnecessary mass force-close - |of channels that may be caused by a bug in Eclair, or issues like running out of disk space, etc. - | - |You should get in touch with Eclair developers and provide logs of your node for analysis. - |""".stripMargin, cause) - sys.exit(1) - stop(FSM.Shutdown) - } - } - case _ => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state - } - } - - private def handleRemoteError(e: Error, d: ChannelData) = { - // see BOLT 1: only print out data verbatim if is composed of printable ASCII characters - log.error(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}") - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, RemoteError(e), isFatal = true)) - - d match { - case _: DATA_CLOSING => stay() // nothing to do, there is already a spending tx published - case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that - handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) - case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) - case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs - } - } - /** * Return full information about a known closing tx. */ @@ -2386,317 +1894,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo proposedTx_opt.get.unsignedTx.copy(tx = tx) } - private def handleMutualClose(closingTx: ClosingTx, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { - log.info(s"closing tx published: closingTxId=${closingTx.tx.txid}") - val nextData = d match { - case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) - case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) - } - goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isFunder) - } - - private def doPublish(closingTx: ClosingTx, isFunder: Boolean): Unit = { - // the funder pays the fee - val fee = if (isFunder) closingTx.fee else 0.sat - txPublisher ! PublishFinalTx(closingTx, fee, None) - blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.minDepthBlocks) - } - - private def spendLocalCurrent(d: HasCommitments) = { - val outdatedCommitment = d match { - case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true - case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true - case _ => false - } - if (outdatedCommitment) { - log.warning("we have an outdated commitment: will not publish our local tx") - stay() - } else { - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) - } - } - - /** - * This helper method will publish txs only if they haven't yet reached minDepth - */ - private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) - process.foreach { publishTx => txPublisher ! publishTx } - skip.foreach(publishTx => log.info("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) - } - - /** - * This helper method will watch txs only if they haven't yet reached minDepth - */ - private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.minDepthBlocks)) - skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) - } - - /** - * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent - * - * @param parentTx transaction which outputs will be watched - * @param outputs outputs that will be watched. They must be a subset of the outputs of the `parentTx` - */ - private def watchSpentIfNeeded(parentTx: Transaction, outputs: Iterable[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - outputs.foreach { output => - require(output.txid == parentTx.txid && output.index < parentTx.txOut.size, s"output doesn't belong to the given parentTx: output=${output.txid}:${output.index} (expected txid=${parentTx.txid} index < ${parentTx.txOut.size})") - } - val (skip, process) = outputs.partition(irrevocablySpent.contains) - process.foreach(output => blockchain ! WatchOutputSpent(self, parentTx.txid, output.index.toInt, Set.empty)) - skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) - } - - private def doPublish(localCommitPublished: LocalCommitPublished, commitments: Commitments): Unit = { - import localCommitPublished._ - - val commitInput = commitments.commitInput.outPoint - val isFunder = commitments.localParams.isFunder - val publishQueue = commitments.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) - case _: Transactions.AnchorOutputsCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) - val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) - } - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - // we also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees - // in the audit DB, even if we restart before confirmation - val watchSpentQueue = htlcTxs.keys ++ claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => tx.input.outPoint } - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleRemoteSpentCurrent(commitTx: Transaction, d: HasCommitments) = { - log.warning(s"they published their current commit in txid=${commitTx.txid}") - require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "remote-commit")) - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - - private def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { - log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "future-remote-commit")) - d.commitments.channelFeatures match { - case ct if ct.paysDirectlyToWallet => - val remoteCommitPublished = RemoteCommitPublished(commitTx, None, Map.empty, List.empty, Map.empty) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address - case _ => - val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - } - - private def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { - log.warning(s"they published their next commit in txid=${commitTx.txid}") - require(d.commitments.remoteNextCommitInfo.isLeft, "next remote commit must be defined") - val Left(waitingForRevocation) = d.commitments.remoteNextCommitInfo - val remoteCommit = waitingForRevocation.nextRemoteCommit - require(commitTx.txid == remoteCommit.txid, "txid mismatch") - - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "next-remote-commit")) - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) - // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - - private def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = { - import remoteCommitPublished._ - - val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) - val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = claimHtlcTxs.keys - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { - log.warning(s"funding tx spent in txid=${tx.txid}") - Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { - case Some(revokedCommitPublished) => - log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isFunder), "revoked-commit")) - val exc = FundingTxSpent(d.channelId, tx) - val error = Error(d.channelId, exc.getMessage) - - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) - // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) - } - goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error - case None => - // the published tx was neither their current commitment nor a revoked one - log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) - goto(ERR_INFORMATION_LEAK) - } - } - - private def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { - import revokedCommitPublished._ - - val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleInformationLeak(tx: Transaction, d: HasCommitments) = { - // this is never supposed to happen !! - log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) - val exc = FundingTxSpent(d.channelId, tx) - val error = Error(d.channelId, exc.getMessage) - - // let's try to spend our current local tx - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - - goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error - } - - private def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: HasCommitments) = { - val exc = PleasePublishYourCommitment(d.channelId) - val error = Error(d.channelId, exc.getMessage) - goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(d.commitments, channelReestablish) storing() sending error - } - - /** - * This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel - */ - private def handleExceptions(s: StateFunction): StateFunction = { - case event if s.isDefinedAt(event) => - try { - s(event) - } catch { - case t: SQLException => - log.error(t, "fatal database error\n") - NotificationsLogger.logFatalError("eclair is shutting down because of a fatal database error", t) - sys.exit(1) - case t: Throwable => handleLocalError(t, event.stateData, None) - } - } - - implicit private def state2mystate(state: FSM.State[ChannelState, ChannelData]): MyState = MyState(state) - - case class MyState(state: FSM.State[ChannelState, ChannelData]) { - - def storing(unused: Unit = ()): FSM.State[ChannelState, ChannelData] = { - state.stateData match { - case d: HasCommitments => - log.debug("updating database record for channelId={}", d.channelId) - nodeParams.db.channels.addOrUpdateChannel(d) - context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) - state - case _ => - log.error(s"can't store data=${state.stateData} in state=${state.stateName}") - state - } - } - - def sending(msgs: Seq[LightningMessage]): FSM.State[ChannelState, ChannelData] = { - msgs.foreach(sending) - state - } - - def sending(msg: LightningMessage): FSM.State[ChannelState, ChannelData] = { - send(msg) - state - } - - /** - * This method allows performing actions during the transition, e.g. after a call to [[MyState.storing]]. This is - * particularly useful to publish transactions only after we are sure that the state has been persisted. - */ - def calling(f: => Unit): FSM.State[ChannelState, ChannelData] = { - f - state - } - - /** - * We don't acknowledge htlc commands immediately, because we send them to the channel as soon as possible, and they - * may not yet have been written to the database. - * - * @param cmd fail/fulfill command that has been processed - */ - def acking(channelId: ByteVector32, cmd: HtlcSettlementCommand): FSM.State[ChannelState, ChannelData] = { - log.debug("scheduling acknowledgement of cmd id={}", cmd.id) - context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommand(nodeParams.db.pendingCommands, channelId, cmd))(context.system.dispatcher) - state - } - - def acking(updates: List[UpdateMessage]): FSM.State[ChannelState, ChannelData] = { - log.debug("scheduling acknowledgement of cmds ids={}", updates.collect { case s: HtlcSettlementMessage => s.id }.mkString(",")) - context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommands(nodeParams.db.pendingCommands, updates))(context.system.dispatcher) - state - } - - } - - private def send(msg: LightningMessage): Unit = { - peer ! Peer.OutgoingMessage(msg, activeConnection) - } - override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) val id = currentMessage match { case INPUT_RESTORED(data) => data.channelId case _ => stateData.channelId } - Logs.mdc(category_opt, remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(id)) + Logs.mdc(category_opt, remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(id), nodeAlias_opt = Some(nodeParams.alias)) } // we let the peer decide what to do diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala new file mode 100644 index 0000000000..14722ddfe6 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -0,0 +1,448 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.pattern.pipe +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script, Transaction} +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.Helpers.{Funding, getRelayFees} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel._ +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.transactions.{Scripts, Transactions} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} +import fr.acinq.eclair.{Features, RealShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import scodec.bits.ByteVector + +import scala.concurrent.duration.DurationInt +import scala.util.{Failure, Success, Try} + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains the state machine for the single-funder channel funding flow. + */ +trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { + + this: Channel => + + /* + FUNDER FUNDEE + | | + | open_channel |WAIT_FOR_OPEN_CHANNEL + |------------------------------->| + WAIT_FOR_ACCEPT_CHANNEL| | + | accept_channel | + |<-------------------------------| + | |WAIT_FOR_FUNDING_CREATED + | funding_created | + |------------------------------->| + WAIT_FOR_FUNDING_SIGNED| | + | funding_signed | + |<-------------------------------| + WAIT_FOR_CHANNEL_READY| |WAIT_FOR_CHANNEL_READY + | channel_ready channel_ready | + |--------------- ---------------| + | \/ | + | /\ | + |<-------------- -------------->| + NORMAL| |NORMAL + */ + + when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { + case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => + Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { + case Left(t) => handleLocalError(t, d, Some(open)) + case Right((channelFeatures, remoteShutdownScript)) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None)) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, open.fundingSatoshis) + log.info("will use fundingMinDepth={}", minimumDepth) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty + val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, + dustLimitSatoshis = localParams.dustLimit, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), + minimumDepth = minimumDepth.getOrElse(0), + htlcMinimumMsat = localParams.htlcMinimum, + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubkey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + tlvStream = TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(channelType) + )) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = open.dustLimitSatoshis, + maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = Some(open.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance + htlcMinimum = open.htlcMinimumMsat, + toSelfDelay = open.toSelfDelay, + maxAcceptedHtlcs = open.maxAcceptedHtlcs, + fundingPubKey = open.fundingPubkey, + revocationBasepoint = open.revocationBasepoint, + paymentBasepoint = open.paymentBasepoint, + delayedPaymentBasepoint = open.delayedPaymentBasepoint, + htlcBasepoint = open.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept + } + + case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { + case Left(t) => + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(t, d, Some(accept)) + case Right((channelFeatures, remoteShutdownScript)) => + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = accept.dustLimitSatoshis, + maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = Some(accept.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance + htlcMinimum = accept.htlcMinimumMsat, + toSelfDelay = accept.toSelfDelay, + maxAcceptedHtlcs = accept.maxAcceptedHtlcs, + fundingPubKey = accept.fundingPubkey, + revocationBasepoint = accept.revocationBasepoint, + paymentBasepoint = accept.paymentBasepoint, + delayedPaymentBasepoint = accept.delayedPaymentBasepoint, + htlcBasepoint = accept.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + log.info("remote will use fundingMinDepth={}", accept.minimumDepth) + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) + wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.lastSent.temporaryChannelId))) + handleFastClose(c, d.lastSent.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => + // let's create the first commitment tx that spends the yet uncommitted funding tx + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + case Left(ex) => handleLocalError(ex, d, None) + case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => + require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelFeatures.commitmentFormat) + // signature of their initial commitment tx that pays remote pushMsat + val fundingCreated = FundingCreated( + temporaryChannelId = temporaryChannelId, + fundingTxid = fundingTx.hash, + fundingOutputIndex = fundingTxOutputIndex, + signature = localSigOfRemoteTx + ) + val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + // NB: we don't send a ChannelSignatureSent for the first commit + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated + } + + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + log.error(t, s"wallet returned error: ") + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) + handleFastClose(c, d.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => + // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + case Left(ex) => handleLocalError(ex, d, None) + case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => + // check remote signature validity + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + Transactions.checkSpendable(signedLocalCommitTx) match { + case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) + case Success(_) => + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelFeatures.commitmentFormat) + val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) + // watch the funding tx transaction + val commitInput = localCommitTx.input + val fundingSigned = FundingSigned( + channelId = channelId, + signature = localSigOfRemoteTx + ) + val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, + LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), + LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 0L, remoteNextHtlcId = 0L, + originChannels = Map.empty, + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, + commitInput, ShaChain.init) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + // NB: we don't send a ChannelSignatureSent for the first commit + log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + watchFundingTx(commitments) + Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending Seq(fundingSigned, channelReady) + } + } + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_CREATED) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) + handleFastClose(c, d.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { + case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => + // we make sure that their sig checks out and that our first commit tx is spendable + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + Transactions.checkSpendable(signedLocalCommitTx) match { + case Failure(cause) => + // we rollback the funding tx, it will never be published + wallet.rollback(fundingTx) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) + case Success(_) => + val commitInput = localCommitTx.input + val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, + LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), remoteCommit, + LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 0L, remoteNextHtlcId = 0L, + originChannels = Map.empty, + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array + commitInput, ShaChain.init) + val blockHeight = nodeParams.currentBlockHeight + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + watchFundingTx(commitments) + + // we will publish the funding tx only after the channel state has been written to disk because we want to + // make sure we first persist the commitment that returns back the funds to us in case of problem + def publishFundingTx(): Unit = { + wallet.commit(fundingTx).onComplete { + case Success(true) => + context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(channelId))) + case Success(false) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) + self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published + case Failure(t) => + channelOpenReplyToUser(Left(LocalError(t))) + log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast + } + } + + Funding.minDepthFunder(commitments.channelFeatures) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady calling publishFundingTx() + } + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { + case Event(remoteChannelReady: ChannelReady, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + if (remoteChannelReady.alias_opt.isDefined && d.commitments.localParams.isInitiator) { + log.info("this chanel isn't zero-conf, but we are funder and they sent an early channel_ready with an alias: no need to wait for confirmations") + // No need to emit ShortChannelIdAssigned: we will emit it when handling their channel_ready in WAIT_FOR_CHANNEL_READY + val (shortIds, localChannelReady) = acceptFundingTx(d.commitments, RealScidStatus.Unknown) + self ! remoteChannelReady + // NB: we will receive a WatchFundingConfirmedTriggered later that will simply be ignored + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(d.commitments, shortIds, localChannelReady) storing() sending localChannelReady + } else { + log.info("received their channel_ready, deferring message") + stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected + } + + case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => + Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + case Success(_) => + log.info(s"channelId=${d.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") + if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) + + val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) + val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) + deferred.foreach(self ! _) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady + case Failure(t) => + log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") + goto(CLOSED) + } + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + + case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) + + case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx match { + case Some(_) => stay() // we are funder, we're still waiting for the funding tx to be confirmed + case None if c.blockHeight - d.waitingSince > FUNDING_TIMEOUT_FUNDEE => + log.warning(s"funding tx hasn't been published in ${c.blockHeight - d.waitingSince} blocks") + self ! BITCOIN_FUNDING_TIMEOUT + stay() + case None => stay() // let's wait longer + } + + case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) + }) + + when(WAIT_FOR_CHANNEL_READY)(handleExceptions { + case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_CHANNEL_READY) => + val shortIds1 = d.shortIds.copy(remoteAlias_opt = channelReady.alias_opt) + shortIds1.remoteAlias_opt.foreach { remoteAlias => + log.info("received remoteAlias={}", remoteAlias) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.commitments.channelId, shortIds = shortIds1, remoteNodeId = remoteNodeId)) + } + log.info("shortIds: real={} localAlias={} remoteAlias={}", shortIds1.real.toOption.getOrElse("none"), shortIds1.localAlias, shortIds1.remoteAlias_opt.getOrElse("none")) + // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced + val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1.localAlias) + log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) + val relayFees = getRelayFees(nodeParams, remoteNodeId, d.commitments) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network + context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) + // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + goto(NORMAL) using DATA_NORMAL(d.commitments.copy(remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)), shortIds1, None, initialChannelUpdate, None, None, None) storing() + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_CHANNEL_READY) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) => handleInformationLeak(tx, d) + + case Event(e: Error, d: DATA_WAIT_FOR_CHANNEL_READY) => handleRemoteError(e, d) + }) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala new file mode 100644 index 0000000000..3fd98cd9a3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -0,0 +1,102 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import akka.actor.FSM +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.PendingCommandsDb +import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.wire.protocol.{HtlcSettlementMessage, LightningMessage, UpdateMessage} + +import scala.concurrent.duration.DurationInt + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains utility functions for basic channel tasks. + */ +trait CommonHandlers { + + this: Channel => + + def send(msg: LightningMessage): Unit = { + peer ! Peer.OutgoingMessage(msg, activeConnection) + } + + implicit def state2mystate(state: FSM.State[ChannelState, ChannelData]): MyState = MyState(state) + + /** + * We wrap the FSM state to add some utility functions that can be called on state transitions. + */ + case class MyState(state: FSM.State[ChannelState, ChannelData]) { + + def storing(unused: Unit = ()): FSM.State[ChannelState, ChannelData] = { + state.stateData match { + case d: PersistentChannelData => + log.debug("updating database record for channelId={}", d.channelId) + nodeParams.db.channels.addOrUpdateChannel(d) + context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) + state + case _: TransientChannelData => + log.error(s"can't store data=${state.stateData} in state=${state.stateName}") + state + } + } + + def sending(msgs: Seq[LightningMessage]): FSM.State[ChannelState, ChannelData] = { + msgs.foreach(sending) + state + } + + def sending(msg: LightningMessage): FSM.State[ChannelState, ChannelData] = { + send(msg) + state + } + + /** + * This method allows performing actions during the transition, e.g. after a call to [[MyState.storing]]. This is + * particularly useful to publish transactions only after we are sure that the state has been persisted. + */ + def calling(f: => Unit): FSM.State[ChannelState, ChannelData] = { + f + state + } + + /** + * We don't acknowledge htlc commands immediately, because we send them to the channel as soon as possible, and they + * may not yet have been written to the database. + * + * @param cmd fail/fulfill command that has been processed + */ + def acking(channelId: ByteVector32, cmd: HtlcSettlementCommand): FSM.State[ChannelState, ChannelData] = { + log.debug("scheduling acknowledgement of cmd id={}", cmd.id) + context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommand(nodeParams.db.pendingCommands, channelId, cmd))(context.system.dispatcher) + state + } + + def acking(updates: List[UpdateMessage]): FSM.State[ChannelState, ChannelData] = { + log.debug("scheduling acknowledgement of cmds ids={}", updates.collect { case s: HtlcSettlementMessage => s.id }.mkString(",")) + context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommands(nodeParams.db.pendingCommands, updates))(context.system.dispatcher) + state + } + + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala new file mode 100644 index 0000000000..c027bc106e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -0,0 +1,362 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.actor.{ActorRef, FSM} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction} +import fr.acinq.eclair.NotificationsLogger +import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} +import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.ClosingTx +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel} + +import java.sql.SQLException + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains handlers for error scenarios (channel closing, force-closing, unhandled, exceptions, etc). + */ +trait ErrorHandlers extends CommonHandlers { + + this: Channel => + + def handleFastClose(c: CloseCommand, channelId: ByteVector32) = { + val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo + replyTo ! RES_SUCCESS(c, channelId) + goto(CLOSED) + } + + def handleMutualClose(closingTx: ClosingTx, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { + log.info(s"closing tx published: closingTxId=${closingTx.tx.txid}") + val nextData = d match { + case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) + case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) + } + goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isInitiator) + } + + def doPublish(closingTx: ClosingTx, isInitiator: Boolean): Unit = { + // the initiator pays the fee + val fee = if (isInitiator) closingTx.fee else 0.sat + txPublisher ! PublishFinalTx(closingTx, fee, None) + blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) + } + + def handleLocalError(cause: Throwable, d: ChannelData, msg: Option[Any]) = { + cause match { + case _: ForcedLocalCommit => + log.warning(s"force-closing channel at user request") + case _ if msg.exists(_.isInstanceOf[OpenChannel]) || msg.exists(_.isInstanceOf[AcceptChannel]) => + // invalid remote channel parameters are logged as warning + log.warning(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + case _: ChannelException => + log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + case _ => + // unhandled error: we dump the channel data, and print the stack trace + log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData:") + } + + val error = Error(d.channelId, cause.getMessage) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = true)) + + d match { + case dd: PersistentChannelData if Closing.nothingAtStake(dd) => goto(CLOSED) + case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => + log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}") + // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) + case dd: PersistentChannelData => + cause match { + case _: ChannelException => + // known channel exception: we force close using our current commitment + spendLocalCurrent(dd) sending error + case _ => + // unhandled exception: we apply the configured strategy + nodeParams.channelConf.unhandledExceptionStrategy match { + case UnhandledExceptionStrategy.LocalClose => + spendLocalCurrent(dd) sending error + case UnhandledExceptionStrategy.Stop => + log.error("unhandled exception: standard procedure would be to force-close the channel, but eclair has been configured to halt instead.") + NotificationsLogger.logFatalError( + s"""stopping node as configured strategy to unhandled exceptions for nodeId=$remoteNodeId channelId=${d.channelId} + | + |Eclair has been configured to shut down when an unhandled exception happens, instead of requesting a + |force-close from the peer. This gives the operator a chance of avoiding an unnecessary mass force-close + |of channels that may be caused by a bug in Eclair, or issues like running out of disk space, etc. + | + |You should get in touch with Eclair developers and provide logs of your node for analysis. + |""".stripMargin, cause) + sys.exit(1) + stop(FSM.Shutdown) + } + } + case _: TransientChannelData => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state + } + } + + def handleRemoteError(e: Error, d: ChannelData) = { + // see BOLT 1: only print out data verbatim if is composed of printable ASCII characters + log.error(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}") + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, RemoteError(e), isFatal = true)) + + d match { + case _: DATA_CLOSING => stay() // nothing to do, there is already a spending tx published + case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => + // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent + case hasCommitments: PersistentChannelData => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) + case _: TransientChannelData => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs + } + } + + /** + * This helper method will publish txs only if they haven't yet reached minDepth + */ + private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) + process.foreach { publishTx => txPublisher ! publishTx } + skip.foreach(publishTx => log.info("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) + } + + /** + * This helper method will watch txs only if they haven't yet reached minDepth + */ + private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) + process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)) + skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) + } + + /** + * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent + * + * @param parentTx transaction which outputs will be watched + * @param outputs outputs that will be watched. They must be a subset of the outputs of the `parentTx` + */ + private def watchSpentIfNeeded(parentTx: Transaction, outputs: Iterable[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + outputs.foreach { output => + require(output.txid == parentTx.txid && output.index < parentTx.txOut.size, s"output doesn't belong to the given parentTx: output=${output.txid}:${output.index} (expected txid=${parentTx.txid} index < ${parentTx.txOut.size})") + } + val (skip, process) = outputs.partition(irrevocablySpent.contains) + process.foreach(output => blockchain ! WatchOutputSpent(self, parentTx.txid, output.index.toInt, Set.empty)) + skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) + } + + def spendLocalCurrent(d: PersistentChannelData) = { + val outdatedCommitment = d match { + case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true + case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true + case _ => false + } + if (outdatedCommitment) { + log.warning("we have an outdated commitment: will not publish our local tx") + stay() + } else { + val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) + } + } + + def doPublish(localCommitPublished: LocalCommitPublished, commitments: Commitments): Unit = { + import localCommitPublished._ + + val commitInput = commitments.commitInput.outPoint + val isInitiator = commitments.localParams.isInitiator + val publishQueue = commitments.commitmentFormat match { + case Transactions.DefaultCommitmentFormat => + val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isInitiator), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) + case _: Transactions.AnchorOutputsCommitmentFormat => + val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) + val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isInitiator), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + } + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + // we also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees + // in the audit DB, even if we restart before confirmation + val watchSpentQueue = htlcTxs.keys ++ claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => tx.input.outPoint } + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleRemoteSpentCurrent(commitTx: Transaction, d: PersistentChannelData) = { + log.warning(s"they published their current commit in txid=${commitTx.txid}") + require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") + + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "remote-commit")) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { + log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "future-remote-commit")) + val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint + val remoteCommitPublished = RemoteCommitPublished( + commitTx = commitTx, + claimMainOutputTx = Closing.RemoteClose.claimMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), + claimHtlcTxs = Map.empty, + claimAnchorTxs = List.empty, + irrevocablySpent = Map.empty) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def handleRemoteSpentNext(commitTx: Transaction, d: PersistentChannelData) = { + log.warning(s"they published their next commit in txid=${commitTx.txid}") + require(d.commitments.remoteNextCommitInfo.isLeft, "next remote commit must be defined") + val Left(waitingForRevocation) = d.commitments.remoteNextCommitInfo + val remoteCommit = waitingForRevocation.nextRemoteCommit + require(commitTx.txid == remoteCommit.txid, "txid mismatch") + + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "next-remote-commit")) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) + // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = { + import remoteCommitPublished._ + + val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) + val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + val watchSpentQueue = claimHtlcTxs.keys + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleRemoteSpentOther(tx: Transaction, d: PersistentChannelData) = { + log.warning(s"funding tx spent in txid=${tx.txid}") + Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { + case Some(revokedCommitPublished) => + log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isInitiator), "revoked-commit")) + val exc = FundingTxSpent(d.channelId, tx) + val error = Error(d.channelId, exc.getMessage) + + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) + // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) + } + goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error + case None => + // the published tx was neither their current commitment nor a revoked one + log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + goto(ERR_INFORMATION_LEAK) + } + } + + def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { + import revokedCommitPublished._ + + val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleInformationLeak(tx: Transaction, d: PersistentChannelData) = { + // this is never supposed to happen !! + log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + val exc = FundingTxSpent(d.channelId, tx) + val error = Error(d.channelId, exc.getMessage) + + // let's try to spend our current local tx + val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) + + goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error + } + + def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: PersistentChannelData) = { + val exc = PleasePublishYourCommitment(d.channelId) + val error = Error(d.channelId, exc.getMessage) + goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(d.commitments, channelReestablish) storing() sending error + } + + /** + * This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel + */ + def handleExceptions(s: StateFunction): StateFunction = { + case event if s.isDefinedAt(event) => + try { + s(event) + } catch { + case t: SQLException => + log.error(t, "fatal database error\n") + NotificationsLogger.logFatalError("eclair is shutting down because of a fatal database error", t) + sys.exit(1) + case t: Throwable => handleLocalError(t, event.stateData, None) + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala new file mode 100644 index 0000000000..58adea13a7 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -0,0 +1,136 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.eclair.{Alias, BlockHeight, RealShortChannelId, ShortChannelId} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingLost, WatchFundingSpent} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT, FUNDING_TIMEOUT_FUNDEE} +import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReadyTlv, Error, TlvStream} + +import scala.concurrent.duration.DurationInt +import scala.util.{Failure, Success} + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains handlers related to funding channel transactions. + */ +trait FundingHandlers extends CommonHandlers { + + this: Channel => + + /** + * This function is used to return feedback to user at channel opening + */ + def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { + val m = message match { + case Left(LocalError(t)) => Status.Failure(t) + case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) + case Right(s) => s + } + origin_opt.foreach(_ ! m) + } + + def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { + // TODO: should we wait for an acknowledgment from the watcher? + val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs + blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) + // TODO: implement this? (not needed if we use a reasonable min_depth) + //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) + } + + def acceptFundingTx(commitments: Commitments, realScidStatus: RealScidStatus): (ShortIds, ChannelReady) = { + blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + // the alias will use in our peer's channel_update message, the goal is to be able to use our channel as soon + // as it reaches NORMAL state, and before it is announced on the network + val shortIds = ShortIds(realScidStatus, ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortIds, remoteNodeId)) + // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway + val channelReady = ChannelReady(commitments.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(shortIds.localAlias))) + (shortIds, channelReady) + } + + /** + * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction + * that we made for some reason). If the funding tx has been double spent we can forget about the channel. + */ + def checkDoubleSpent(fundingTx: Transaction): Unit = { + log.debug(s"checking status of funding tx txid=${fundingTx.txid}") + wallet.doubleSpent(fundingTx).onComplete { + case Success(true) => + log.warning(s"funding tx has been double spent! fundingTxid=${fundingTx.txid} fundingTx=$fundingTx") + self ! BITCOIN_FUNDING_PUBLISH_FAILED + case Success(false) => () + case Failure(t) => log.error(t, s"error while testing status of funding tx fundingTxid=${fundingTx.txid}: ") + } + } + + def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSince: BlockHeight, fundingTx_opt: Option[Transaction]) = { + import getTxResponse._ + tx_opt match { + case Some(_) => () // the funding tx exists, nothing to do + case None => + fundingTx_opt match { + case Some(fundingTx) => + // if we are funder, we never give up + // we cannot correctly set the fee, but it was correctly set when we initially published the transaction + log.info(s"republishing the funding tx...") + txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, "funding", 0 sat, None) + // we also check if the funding tx has been double-spent + checkDoubleSpent(fundingTx) + context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) + case None if (nodeParams.currentBlockHeight - waitingSince) > FUNDING_TIMEOUT_FUNDEE => + // if we are fundee, we give up after some time + log.warning(s"funding tx hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks") + self ! BITCOIN_FUNDING_TIMEOUT + case None => + // let's wait a little longer + log.info(s"funding tx still hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks, will wait ${FUNDING_TIMEOUT_FUNDEE - (nodeParams.currentBlockHeight - waitingSince)} more blocks...") + context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) + } + } + stay() + } + + def handleFundingPublishFailed(d: PersistentChannelData) = { + log.error(s"failed to publish funding tx") + val exc = ChannelFundingError(d.channelId) + val error = Error(d.channelId, exc.getMessage) + // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: + // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) + goto(CLOSED) sending error + } + + def handleFundingTimeout(d: PersistentChannelData) = { + log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") + val exc = FundingTxTimedout(d.channelId) + val error = Error(d.channelId, exc.getMessage) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) + goto(CLOSED) sending error + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala index bc09975151..e712e49f3c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala @@ -102,7 +102,7 @@ private class FinalTxPublisher(nodeParams: NodeParams, case ParentTxOk => publish() case ParentTxMissing => log.debug("parent tx is missing, retrying after delay...") - timers.startSingleTimer(CheckParentTx, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckParentTx, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same case UnknownFailure(reason) => log.error("could not check parent tx: ", reason) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index afbe247f86..5ae49bd53f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.publish.TxPublisher.{TxPublishContext, TxRejectedReason} @@ -44,6 +44,7 @@ object MempoolTxMonitor { private case class InputStatus(spentConfirmed: Boolean, spentUnconfirmed: Boolean) extends Command private case class CheckInputFailed(reason: Throwable) extends Command private case class TxConfirmations(confirmations: Int, blockHeight: BlockHeight) extends Command + private case class ParentTxStatus(confirmed: Boolean, blockHeight: BlockHeight) extends Command private case object TxNotFound extends Command private case class GetTxConfirmationsFailed(reason: Throwable) extends Command private case class WrappedCurrentBlockHeight(currentBlockHeight: BlockHeight) extends Command @@ -58,7 +59,7 @@ object MempoolTxMonitor { sealed trait TxResult sealed trait IntermediateTxResult extends TxResult /** The transaction is still unconfirmed and available in the mempool. */ - case class TxInMempool(txid: ByteVector32, blockHeight: BlockHeight) extends IntermediateTxResult + case class TxInMempool(txid: ByteVector32, blockHeight: BlockHeight, parentConfirmed: Boolean) extends IntermediateTxResult /** The transaction is confirmed, but hasn't reached min depth yet, we should wait for more confirmations. */ case class TxRecentlyConfirmed(txid: ByteVector32, confirmations: Int) extends IntermediateTxResult sealed trait FinalTxResult extends TxResult @@ -101,7 +102,6 @@ private class MempoolTxMonitor(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case PublishOk => log.debug("txid={} was successfully published, waiting for confirmation...", cmd.tx.txid) - context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc)) waitForConfirmation() case PublishFailed(reason) if reason.getMessage.contains("rejecting replacement") => log.info("could not publish tx: a conflicting mempool transaction is already in the mempool") @@ -122,8 +122,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams, log.info("could not publish tx: a conflicting mempool transaction is already in the mempool") sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed)) } else { - log.info("could not publish tx: one of our wallet inputs is not available") - sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone)) + log.info("could not publish tx: one of our inputs cannot be found") + sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone)) } case CheckInputFailed(reason) => log.error("could not check input status: ", reason) @@ -134,9 +134,10 @@ private class MempoolTxMonitor(nodeParams: NodeParams, def waitForConfirmation(): Behavior[Command] = { val messageAdapter = context.messageAdapter[CurrentBlockHeight](cbc => WrappedCurrentBlockHeight(cbc.blockHeight)) context.system.eventStream ! EventStream.Subscribe(messageAdapter) + context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc)) Behaviors.receiveMessagePartial { case WrappedCurrentBlockHeight(currentBlockHeight) => - timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same case CheckTxConfirmations(currentBlockHeight) => context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.tx.txid)) { @@ -147,9 +148,12 @@ private class MempoolTxMonitor(nodeParams: NodeParams, Behaviors.same case TxConfirmations(confirmations, currentBlockHeight) => if (confirmations == 0) { - cmd.replyTo ! TxInMempool(cmd.tx.txid, currentBlockHeight) + context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.input.txid)) { + case Success(parentConfirmations_opt) => ParentTxStatus(parentConfirmations_opt.exists(_ >= 1), currentBlockHeight) + case Failure(reason) => GetTxConfirmationsFailed(reason) + } Behaviors.same - } else if (confirmations < nodeParams.minDepthBlocks) { + } else if (confirmations < nodeParams.channelConf.minDepthBlocks) { log.info("txid={} has {} confirmations, waiting to reach min depth", cmd.tx.txid, confirmations) cmd.replyTo ! TxRecentlyConfirmed(cmd.tx.txid, confirmations) Behaviors.same @@ -158,6 +162,9 @@ private class MempoolTxMonitor(nodeParams: NodeParams, context.system.eventStream ! EventStream.Publish(TransactionConfirmed(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx)) sendFinalResult(TxDeeplyBuried(cmd.tx), Some(messageAdapter)) } + case ParentTxStatus(confirmed, currentBlockHeight) => + cmd.replyTo ! TxInMempool(cmd.tx.txid, currentBlockHeight, confirmed) + Behaviors.same case TxNotFound => log.warn("txid={} has been evicted from the mempool", cmd.tx.txid) checkInputStatus(cmd.input) @@ -174,8 +181,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams, log.info("tx was evicted from the mempool: a conflicting transaction replaced it") sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed)) } else { - log.info("tx was evicted from the mempool: one of our wallet inputs disappeared") - sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone)) + log.info("tx was evicted from the mempool: one of our inputs disappeared") + sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone)) } case CheckInputFailed(reason) => log.error("could not check input status: ", reason) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 8a75ef4972..7edffd673b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.FundTransactionOptions diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala index beed33a4a9..66fcb3fe82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Transaction} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext @@ -181,7 +181,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams, // - their commit is not confirmed: if it is, there is no need to publish our htlc transactions // - if this is an htlc-success transaction, we have the preimage context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.commitments.remoteCommit.txid)) { - case Success(Some(depth)) if depth >= nodeParams.minDepthBlocks => RemoteCommitTxConfirmed + case Success(Some(depth)) if depth >= nodeParams.channelConf.minDepthBlocks => RemoteCommitTxConfirmed case Success(_) => ParentTxOk case Failure(reason) => UnknownFailure(reason) } @@ -243,7 +243,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams, // - our commit is not confirmed: if it is, there is no need to publish our claim-htlc transactions // - if this is a claim-htlc-success transaction, we have the preimage context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid)) { - case Success(Some(depth)) if depth >= nodeParams.minDepthBlocks => LocalCommitTxConfirmed + case Success(Some(depth)) if depth >= nodeParams.channelConf.minDepthBlocks => LocalCommitTxConfirmed case Success(_) => ParentTxOk case Failure(reason) => UnknownFailure(reason) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 20a837a61e..ca68a9479a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -18,13 +18,14 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratePerKw} import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.FundedTx import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher.{ClaimLocalAnchorWithWitnessData, ReplaceableTxWithWitnessData} import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.{BlockHeight, NodeParams} import scala.concurrent.duration.{DurationInt, DurationLong} @@ -195,10 +196,17 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case WrappedTxResult(txResult) => txResult match { - case MempoolTxMonitor.TxInMempool(_, currentBlockHeight) => - context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { - case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight) - case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst + case MempoolTxMonitor.TxInMempool(_, currentBlockHeight, parentConfirmed) => + val shouldRbf = cmd.txInfo match { + // Our commit tx was confirmed on its own, so there's no need to increase fees on the anchor tx. + case _: Transactions.ClaimLocalAnchorOutputTx if parentConfirmed => false + case _ => true + } + if (shouldRbf) { + context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { + case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight) + case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst + } } Behaviors.same case MempoolTxMonitor.TxRecentlyConfirmed(_, _) => Behaviors.same // just wait for the tx to be deeply buried @@ -220,7 +228,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, None } // We avoid a herd effect whenever we fee bump transactions. - targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)) Behaviors.same case BumpFee(targetFeerate) => fundReplacement(targetFeerate, tx) case UpdateConfirmationTarget(target) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index edca99afde..043f1387cf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -107,8 +107,8 @@ object TxPublisher { object TxRejectedReason { /** We don't have enough funds in our wallet to reach the given feerate. */ case object CouldNotFund extends TxRejectedReason - /** The transaction was published but then evicted from the mempool, because one of its wallet inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */ - case object WalletInputGone extends TxRejectedReason + /** The transaction was published but then evicted from the mempool, because one of its inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */ + case object InputGone extends TxRejectedReason /** A conflicting transaction spending the same input has been confirmed. */ case object ConflictingTxConfirmed extends TxRejectedReason /** A conflicting transaction spending the same input is in the mempool and we failed to replace it. */ @@ -255,12 +255,19 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact stopAttempts(rejectedAttempts) val pending2 = if (remainingAttempts.isEmpty) pending - cmd.input else pending + (cmd.input -> remainingAttempts) reason match { - case TxRejectedReason.WalletInputGone => + case TxRejectedReason.InputGone => // Our transaction has been evicted from the mempool because it depended on an unconfirmed input that has - // been replaced. We should be able to retry right now with new wallet inputs (no need to wait for a new - // block). - timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) - run(pending2, retryNextBlock, channelContext) + // been replaced. + cmd match { + case _: PublishReplaceableTx => + // We should be able to retry right now with new wallet inputs (no need to wait for a new block). + timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) + run(pending2, retryNextBlock, channelContext) + case _: PublishFinalTx => + // The transaction cannot be replaced, so there is no point in retrying immediately, let's wait until + // the next block to see if our input comes back to the mempool. + run(pending2, retryNextBlock ++ rejectedAttempts.map(_.cmd), channelContext) + } case TxRejectedReason.CouldNotFund => // We don't have enough funds at the moment to afford our target feerate, but it may change once pending // transactions confirm, so we retry when a new block is found. @@ -293,7 +300,7 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact case WrappedCurrentBlockHeight(currentBlockHeight) => if (retryNextBlock.nonEmpty) { log.info("{} transactions are still pending at block {}, retrying {} transactions that previously failed", pending.size, currentBlockHeight, retryNextBlock.length) - retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)) } run(pending, Seq.empty, channelContext) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala index 90332a07eb..f46c1addba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchParentTxConfirmed, WatchParentTxConfirmedTriggered} @@ -85,7 +85,7 @@ private class TxTimeLocksMonitor(nodeParams: NodeParams, case WrappedCurrentBlockHeight(currentBlockHeight) => if (cltvTimeout <= currentBlockHeight) { context.system.eventStream ! EventStream.Unsubscribe(messageAdapter) - timers.startSingleTimer(CheckRelativeTimeLock, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckRelativeTimeLock, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same } else { Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala index ecdca6e407..2f3f0425bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.{ByteVector32, Protocol} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Protocol} import fr.acinq.eclair.crypto.ChaCha20Poly1305.{DecryptionError, EncryptionError, InvalidCounter} import grizzled.slf4j.Logging import org.bouncycastle.crypto.engines.ChaCha7539Engine diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala index ae14fbdabd..c5dafa7bcd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import scodec.bits.ByteVector /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala index 011a2b9efc..cc95240791 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.macs.HMac import org.bouncycastle.crypto.params.KeyParameter diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala index 0539127122..6ba0fb8dd6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Crypto, Protocol} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Crypto, Protocol} import fr.acinq.eclair.randomBytes import grizzled.slf4j.Logging import org.bouncycastle.crypto.digests.SHA256Digest @@ -64,10 +64,7 @@ object Noise { * @return sha256(publicKey * keyPair.priv in compressed format) */ override def dh(keyPair: KeyPair, publicKey: ByteVector): ByteVector = { - val point = Crypto.curve.getCurve.decodePoint(publicKey.toArray) - val scalar = new BigInteger(1, keyPair.priv.take(32).toArray) - val point1 = point.multiply(scalar).normalize() - Crypto.sha256(ByteVector.view(point1.getEncoded(true))) + Crypto.ecdh(PrivateKey(keyPair.priv), PublicKey(publicKey)) } override def dhLen: Int = 32 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala index 8159d5a10d..08a2464171 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Protocol +import fr.acinq.bitcoin.scalacompat.Protocol import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.engines.ChaCha7539Engine import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala index 4091c9c08e..44dff50dad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.wire.protocol.CommonCodecs import scodec.Codec diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index bd5ec740e9..df485abcd5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.encryptedDataCodec import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.Attempt @@ -68,7 +69,7 @@ object Sphinx extends Logging { * @return a tuple (ephemeral public keys, shared secrets). */ def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = { - val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG), sessionKey.value) + val ephemeralPublicKey0 = blind(PublicKey(fr.acinq.bitcoin.PublicKey.Generator), sessionKey.value) val secret0 = computeSharedSecret(publicKeys.head, sessionKey) val blindingFactor0 = computeBlindingFactor(ephemeralPublicKey0, secret0) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemeralPublicKey0), Seq(blindingFactor0), Seq(secret0)) @@ -410,6 +411,19 @@ object Sphinx extends Logging { BlindedRoute(publicKeys.head, blindingKeys.head, blindedHops) } + /** + * Creates a direct blinded route with a single node + * + * @param sessionKey this node's session key. + * @param nodeId public key of the target node, which is also the introduction point. + * @param selfId payloads that should be encrypted for each node on the route. + * @return a blinded route. + */ + def createDirect(sessionKey: PrivateKey, nodeId: PublicKey, selfId: ByteVector): BlindedRoute = { + val selfPayload = encryptedDataCodec.encode(TlvStream(Seq(RouteBlindingEncryptedDataTlv.PathId(selfId)))).require.bytes + Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)) + } + /** * Compute the blinded private key that must be used to decrypt an incoming blinded onion. * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 1a333c8c78..bb536ada3a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -21,8 +21,8 @@ import akka.event.Logging.MDC import akka.event._ import akka.io.Tcp import akka.util.ByteString -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Protocol +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Protocol import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.ChaCha20Poly1305.ChaCha20Poly1305Error import fr.acinq.eclair.crypto.Noise._ @@ -94,7 +94,8 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co makeReader(keyPair) } - def sendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = { + def decodeAndSendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = { + log.debug("decoding {} plaintext messages", plaintextMessages.size) var m: Map[T, Int] = Map() plaintextMessages.foreach(plaintext => Try(codec.decode(plaintext.toBitVector)) match { case Success(Attempt.Successful(DecodeResult(message, _))) => @@ -106,6 +107,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Failure(t) => log.error(s"cannot deserialize $plaintext: ${t.getMessage}") }) + log.debug("decoded {} messages", m.values.sum) m } @@ -164,31 +166,28 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Event(Listener(listener), d@WaitingForListenerData(_, dec)) => context.watch(listener) val (dec1, plaintextMessages) = dec.decrypt() - if (plaintextMessages.isEmpty) { + val unackedReceived1 = decodeAndSendToListener(listener, plaintextMessages) + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") connection ! Tcp.ResumeReading - goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = Map.empty[T, Int], unackedSent = None) - } else { - log.debug(s"read ${plaintextMessages.size} messages, waiting for readacks") - val unackedReceived = sendToListener(listener, plaintextMessages) - goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived, unackedSent = None) } + goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = unackedReceived1, unackedSent = None) } } when(Normal) { handleExceptions { - case Event(Tcp.Received(data), d: NormalData[T]) => + case Event(Tcp.Received(data), d: NormalData[T@unchecked]) => + log.debug("received chunk of size={}", data.size) val (dec1, plaintextMessages) = d.decryptor.copy(buffer = d.decryptor.buffer ++ data).decrypt() - if (plaintextMessages.isEmpty) { + val unackedReceived1 = decodeAndSendToListener(d.listener, plaintextMessages) + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") connection ! Tcp.ResumeReading - stay() using d.copy(decryptor = dec1) - } else { - log.debug("read {} messages, waiting for readacks", plaintextMessages.size) - val unackedReceived = sendToListener(d.listener, plaintextMessages) - stay() using NormalData(d.encryptor, dec1, d.listener, d.sendBuffer, unackedReceived, d.unackedSent) } + stay() using d.copy(decryptor = dec1, unackedReceived = unackedReceived1) - case Event(ReadAck(msg: T), d: NormalData[T]) => + case Event(ReadAck(msg: T), d: NormalData[T@unchecked]) => // how many occurences of this message are still unacked? val remaining = d.unackedReceived.getOrElse(msg, 0) - 1 log.debug("acking message {}", msg) @@ -197,13 +196,12 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co if (unackedReceived1.isEmpty) { log.debug("last incoming message was acked, resuming reading") connection ! Tcp.ResumeReading - stay() using d.copy(unackedReceived = unackedReceived1) } else { log.debug("still waiting for readacks, unacked={}", unackedReceived1) - stay() using d.copy(unackedReceived = unackedReceived1) } + stay() using d.copy(unackedReceived = unackedReceived1) - case Event(t: T, d: NormalData[T]) => + case Event(t: T, d: NormalData[T@unchecked]) => if (d.sendBuffer.normalPriority.size + d.sendBuffer.lowPriority.size >= MAX_BUFFERED) { log.warning("send buffer overrun, closing connection") connection ! PoisonPill @@ -224,7 +222,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co stay() using d.copy(encryptor = enc1, unackedSent = Some(t)) } - case Event(WriteAck, d: NormalData[T]) => + case Event(WriteAck, d: NormalData[T@unchecked]) => def send(t: T) = { diag(t, "OUT") val blob = codec.encode(t).require.toByteVector @@ -262,7 +260,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Event(msg, d) => d match { - case n: NormalData[T] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") + case n: NormalData[_] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") case _ => log.warning(s"unhandled message $msg in state ${d.getClass.getSimpleName}") } stay() @@ -410,6 +408,7 @@ object TransportHandler { case class Encryptor(state: CipherState) { /** * see BOLT #8 + * {{{ * +------------------------------- * |2-byte encrypted message length| * +------------------------------- @@ -425,6 +424,7 @@ object TransportHandler { * | 16-byte MAC of the | * | lightning message | * +------------------------------- + * }}} * * @param plaintext plaintext * @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala index 69a686ea57..3f0c53d070 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.crypto import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.NewBlock import fr.acinq.eclair.channel.ChannelSignatureReceived diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index b1979d3a19..e5e4bfdfb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.{ByteVector64, Crypto, DeterministicWallet, Protocol} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, Protocol} import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import scodec.bits.ByteVector @@ -52,12 +52,12 @@ trait ChannelKeyManager { } /** - * @param isFunder true if we're funding this channel + * @param isInitiator true if we initiated the channel open * @return a partial key path for a new funding public key. This key path will be extended: * - with a specific "chain" prefix * - with a specific "funding pubkey" suffix */ - def newFundingKeyPath(isFunder: Boolean): DeterministicWallet.KeyPath + def newFundingKeyPath(isInitiator: Boolean): DeterministicWallet.KeyPath /** * @param tx input transaction diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 9891884b65..826aeb928c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.crypto.keymanager import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{derivePrivateKey, _} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Announcements @@ -58,7 +58,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath)) }) - private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalChannelKeyManager.keyBasePath(chainHash) ++ channelKeyPath.path) :+ index + private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): KeyPath = KeyPath((LocalChannelKeyManager.keyBasePath(chainHash) ++ channelKeyPath.path) :+ index) private def fundingPrivateKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPrivateKey = privateKeys.get(internalKeyPath(channelKeyPath, hardened(0))) @@ -72,8 +72,8 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath): ByteVector32 = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) - override def newFundingKeyPath(isFunder: Boolean): KeyPath = { - val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) + override def newFundingKeyPath(isInitiator: Boolean): KeyPath = { + val last = DeterministicWallet.hardened(if (isInitiator) 1 else 0) def next(): Long = randomLong() & 0xFFFFFFFFL diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala index 41750578b5..3ffd713798 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.router.Announcements import grizzled.slf4j.Logging import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala index 3b478aa09d..078b2bfb8c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet} import scodec.bits.ByteVector trait NodeKeyManager { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index 6645430e6c..2105278cb4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -16,17 +16,15 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} -import fr.acinq.eclair.{TimestampMilli, TimestampSecond} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.{PathFindingExperimentMetrics, PaymentReceived, PaymentRelayed, PaymentSent} -import java.io.Closeable - -trait AuditDb extends Closeable { +trait AuditDb { def add(channelLifecycle: ChannelEvent): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index b676b71a24..a771f927cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -16,24 +16,22 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.DbEventHandler.ChannelEvent -import java.io.Closeable +trait ChannelsDb { -trait ChannelsDb extends Closeable { + def addOrUpdateChannel(data: PersistentChannelData): Unit - def addOrUpdateChannel(state: HasCommitments): Unit - - def getChannel(channelId: ByteVector32): Option[HasCommitments] + def getChannel(channelId: ByteVector32): Option[PersistentChannelData] def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit def removeChannel(channelId: ByteVector32): Unit - def listLocalChannels(): Seq[HasCommitments] + def listLocalChannels(): Seq[PersistentChannelData] def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 2e78f6d9d1..96e30d5c30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -21,6 +21,7 @@ import akka.actor.{ActorSystem, CoordinatedShutdown} import com.typesafe.config.Config import com.zaxxer.hikari.{HikariConfig, HikariDataSource} import fr.acinq.eclair.TimestampMilli +import fr.acinq.eclair.db.migration.{CompareDb, MigrateDb} import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils._ import fr.acinq.eclair.db.pg._ @@ -42,6 +43,7 @@ trait Databases { def channels: ChannelsDb def peers: PeersDb def payments: PaymentsDb + def offers: OffersDb def pendingCommands: PendingCommandsDb //@formatter:on } @@ -73,15 +75,18 @@ object Databases extends Logging { } object SqliteDatabases { - def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection): SqliteDatabases = SqliteDatabases( - network = new SqliteNetworkDb(networkJdbc), - audit = new SqliteAuditDb(auditJdbc), - channels = new SqliteChannelsDb(eclairJdbc), - peers = new SqlitePeersDb(eclairJdbc), - payments = new SqlitePaymentsDb(eclairJdbc), - pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), - backupConnection = eclairJdbc - ) + def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { + jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _)) + SqliteDatabases( + network = new SqliteNetworkDb(networkJdbc), + audit = new SqliteAuditDb(auditJdbc), + channels = new SqliteChannelsDb(eclairJdbc), + peers = new SqlitePeersDb(eclairJdbc), + payments = new SqlitePaymentsDb(eclairJdbc), + pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), + backupConnection = eclairJdbc + ) + } } case class PostgresDatabases private(network: PgNetworkDb, @@ -241,19 +246,22 @@ object Databases extends Logging { databases } + } - private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { - def readString(path: Path): String = Files.readAllLines(path).get(0) + /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ + case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`. If this was intended, make sure you have migrated your data, otherwise your channels will be force-closed and you may lose funds.") - def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { + def readString(path: Path): String = Files.readAllLines(path).get(0) - if (urlFile.exists()) { - val oldUrl = readString(urlFile.toPath) - if (oldUrl != url) - throw JdbcUrlChanged(oldUrl, url) - } else { - writeString(urlFile.toPath, url) - } + def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + + if (urlFile.exists()) { + val oldUrl = readString(urlFile.toPath) + if (oldUrl != url) + throw JdbcUrlChanged(oldUrl, url) + } else { + writeString(urlFile.toPath, url) } } @@ -261,13 +269,25 @@ object Databases extends Logging { db match { case Some(d) => d case None => + val jdbcUrlFile = new File(chaindir, "last_jdbcurl") dbConfig.getString("driver") match { - case "sqlite" => Databases.sqlite(chaindir) - case "postgres" => Databases.postgres(dbConfig, instanceId, chaindir) - case "dual" => - val sqlite = Databases.sqlite(chaindir) - val postgres = Databases.postgres(dbConfig, instanceId, chaindir) - DualDatabases(sqlite, postgres) + case "sqlite" => Databases.sqlite(chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) + case "postgres" => Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) + case dual@("dual-sqlite-primary" | "dual-postgres-primary") => + logger.info(s"using $dual database mode") + val sqlite = Databases.sqlite(chaindir, jdbcUrlFile_opt = None) + val postgres = Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = None) + val (primary, secondary) = if (dual == "dual-sqlite-primary") (sqlite, postgres) else (postgres, sqlite) + val dualDb = DualDatabases(primary, secondary) + if (primary == sqlite) { + if (dbConfig.getBoolean("dual.migrate-on-restart")) { + MigrateDb.migrateAll(dualDb) + } + if (dbConfig.getBoolean("dual.compare-on-restart")) { + CompareDb.compareAll(dualDb) + } + } + dualDb case driver => throw new RuntimeException(s"unknown database driver `$driver`") } } @@ -276,16 +296,17 @@ object Databases extends Logging { /** * Given a parent folder it creates or loads all the databases from a JDBC connection */ - def sqlite(dbdir: File): SqliteDatabases = { + def sqlite(dbdir: File, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { dbdir.mkdirs() SqliteDatabases( eclairJdbc = SqliteUtils.openSqliteFile(dbdir, "eclair.sqlite", exclusiveLock = true, journalMode = "wal", syncFlag = "full"), // there should only be one process writing to this file networkJdbc = SqliteUtils.openSqliteFile(dbdir, "network.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal"), // we don't need strong durability guarantees on the network db - auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full") + auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full"), + jdbcUrlFile_opt = jdbcUrlFile_opt ) } - def postgres(dbConfig: Config, instanceId: UUID, dbdir: File, lockExceptionHandler: LockFailureHandler = LockFailureHandler.logAndStop)(implicit system: ActorSystem): PostgresDatabases = { + def postgres(dbConfig: Config, instanceId: UUID, dbdir: File, jdbcUrlFile_opt: Option[File], lockExceptionHandler: LockFailureHandler = LockFailureHandler.logAndStop)(implicit system: ActorSystem): PostgresDatabases = { dbdir.mkdirs() val database = dbConfig.getString("postgres.database") val host = dbConfig.getString("postgres.host") @@ -320,8 +341,6 @@ object Databases extends Logging { case unknownLock => throw new RuntimeException(s"unknown postgres lock type: `$unknownLock`") } - val jdbcUrlFile = new File(dbdir, "last_jdbcurl") - val safetyChecks_opt = if (dbConfig.getBoolean("postgres.safety-checks.enabled")) { Some(PostgresDatabases.SafetyChecks( localChannelsMaxAge = FiniteDuration(dbConfig.getDuration("postgres.safety-checks.max-age.local-channels").getSeconds, TimeUnit.SECONDS), @@ -337,7 +356,7 @@ object Databases extends Logging { hikariConfig = hikariConfig, instanceId = instanceId, lock = lock, - jdbcUrlFile_opt = Some(jdbcUrlFile), + jdbcUrlFile_opt = jdbcUrlFile_opt, readOnlyUser_opt = readOnlyUser_opt, resetJsonColumns = resetJsonColumns, safetyChecks_opt = safetyChecks_opt diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index 13a9c1c8f2..a1f63f57ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.db import akka.actor.{Actor, DiagnosticActorLogging, Props} import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel.Monitoring.{Metrics => ChannelMetrics, Tags => ChannelTags} import fr.acinq.eclair.channel._ @@ -108,10 +108,10 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelStateChanged => // NB: order matters! e match { - case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => + case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY, NORMAL, Some(commitments: Commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created - auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isFunder, !commitments.announceChannel, event)) + auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isInitiator, !commitments.announceChannel, event)) channelsDb.updateChannelMeta(channelId, event) case ChannelStateChanged(_, _, _, _, WAIT_FOR_INIT_INTERNAL, _, _) => case ChannelStateChanged(_, channelId, _, _, OFFLINE, SYNCING, _) => @@ -124,7 +124,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelClosed => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Closed).increment() val event = ChannelEvent.EventType.Closed(e.closingType) - auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) + auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isInitiator, !e.commitments.announceChannel, event)) channelsDb.updateChannelMeta(e.channelId, event) case u: ChannelUpdateParametersChanged => @@ -152,7 +152,7 @@ object DbEventHandler { def props(nodeParams: NodeParams): Props = Props(new DbEventHandler(nodeParams)) // @formatter:off - case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isFunder: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) + case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isInitiator: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) object ChannelEvent { sealed trait EventType { def label: String } object EventType { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 9ca1a2ed56..41b226f8a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -1,13 +1,12 @@ package fr.acinq.eclair.db import com.google.common.util.concurrent.ThreadFactoryBuilder -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.DualDatabases.runAsync -import fr.acinq.eclair.db.pg._ -import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router @@ -23,25 +22,30 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} /** - * An implementation of [[Databases]] where there are two separate underlying db, one sqlite and one postgres. - * Sqlite is the main database, but we also replicate all calls to postgres. - * Calls to postgres are made asynchronously in a dedicated thread pool, so that it doesn't have any performance impact. + * An implementation of [[Databases]] where there are two separate underlying db, one primary and one secondary. + * All calls to primary are replicated asynchronously to secondary. + * Calls to secondary are made asynchronously in a dedicated thread pool, so that it doesn't have any performance impact. */ -case class DualDatabases(sqlite: SqliteDatabases, postgres: PostgresDatabases) extends Databases with FileBackup { +case class DualDatabases(primary: Databases, secondary: Databases) extends Databases with FileBackup { - override val network: NetworkDb = DualNetworkDb(sqlite.network, postgres.network) + override val network: NetworkDb = DualNetworkDb(primary.network, secondary.network) - override val audit: AuditDb = DualAuditDb(sqlite.audit, postgres.audit) + override val audit: AuditDb = DualAuditDb(primary.audit, secondary.audit) - override val channels: ChannelsDb = DualChannelsDb(sqlite.channels, postgres.channels) + override val channels: ChannelsDb = DualChannelsDb(primary.channels, secondary.channels) - override val peers: PeersDb = DualPeersDb(sqlite.peers, postgres.peers) + override val peers: PeersDb = DualPeersDb(primary.peers, secondary.peers) - override val payments: PaymentsDb = DualPaymentsDb(sqlite.payments, postgres.payments) + override val payments: PaymentsDb = DualPaymentsDb(primary.payments, secondary.payments) - override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(sqlite.pendingCommands, postgres.pendingCommands) + override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(primary.pendingCommands, secondary.pendingCommands) - override def backup(backupFile: File): Unit = sqlite.backup(backupFile) + /** if one of the database supports file backup, we use it */ + override def backup(backupFile: File): Unit = (primary, secondary) match { + case (f: FileBackup, _) => f.backup(backupFile) + case (_, f: FileBackup) => f.backup(backupFile) + case _ => () + } } object DualDatabases extends Logging { @@ -55,360 +59,338 @@ object DualDatabases extends Logging { throw t } } + + def getDatabases(dualDatabases: DualDatabases): (SqliteDatabases, PostgresDatabases) = + (dualDatabases.primary, dualDatabases.secondary) match { + case (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) => + (sqliteDb, postgresDb) + case (postgresDb: PostgresDatabases, sqliteDb: SqliteDatabases) => + (sqliteDb, postgresDb) + case _ => throw new IllegalArgumentException("there must be one sqlite and one postgres in dual db mode") + } } -case class DualNetworkDb(sqlite: SqliteNetworkDb, postgres: PgNetworkDb) extends NetworkDb { +case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends NetworkDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-network").build())) override def addNode(n: NodeAnnouncement): Unit = { - runAsync(postgres.addNode(n)) - sqlite.addNode(n) + runAsync(secondary.addNode(n)) + primary.addNode(n) } override def updateNode(n: NodeAnnouncement): Unit = { - runAsync(postgres.updateNode(n)) - sqlite.updateNode(n) + runAsync(secondary.updateNode(n)) + primary.updateNode(n) } override def getNode(nodeId: Crypto.PublicKey): Option[NodeAnnouncement] = { - runAsync(postgres.getNode(nodeId)) - sqlite.getNode(nodeId) + runAsync(secondary.getNode(nodeId)) + primary.getNode(nodeId) } override def removeNode(nodeId: Crypto.PublicKey): Unit = { - runAsync(postgres.removeNode(nodeId)) - sqlite.removeNode(nodeId) + runAsync(secondary.removeNode(nodeId)) + primary.removeNode(nodeId) } override def listNodes(): Seq[NodeAnnouncement] = { - runAsync(postgres.listNodes()) - sqlite.listNodes() + runAsync(secondary.listNodes()) + primary.listNodes() } override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = { - runAsync(postgres.addChannel(c, txid, capacity)) - sqlite.addChannel(c, txid, capacity) + runAsync(secondary.addChannel(c, txid, capacity)) + primary.addChannel(c, txid, capacity) } override def updateChannel(u: ChannelUpdate): Unit = { - runAsync(postgres.updateChannel(u)) - sqlite.updateChannel(u) + runAsync(secondary.updateChannel(u)) + primary.updateChannel(u) } override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { - runAsync(postgres.removeChannels(shortChannelIds)) - sqlite.removeChannels(shortChannelIds) + runAsync(secondary.removeChannels(shortChannelIds)) + primary.removeChannels(shortChannelIds) } - override def listChannels(): SortedMap[ShortChannelId, Router.PublicChannel] = { - runAsync(postgres.listChannels()) - sqlite.listChannels() + override def listChannels(): SortedMap[RealShortChannelId, Router.PublicChannel] = { + runAsync(secondary.listChannels()) + primary.listChannels() } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = { - runAsync(postgres.addToPruned(shortChannelIds)) - sqlite.addToPruned(shortChannelIds) + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = { + runAsync(secondary.addToPruned(shortChannelIds)) + primary.addToPruned(shortChannelIds) } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = { - runAsync(postgres.removeFromPruned(shortChannelId)) - sqlite.removeFromPruned(shortChannelId) + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = { + runAsync(secondary.removeFromPruned(shortChannelId)) + primary.removeFromPruned(shortChannelId) } override def isPruned(shortChannelId: ShortChannelId): Boolean = { - runAsync(postgres.isPruned(shortChannelId)) - sqlite.isPruned(shortChannelId) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.isPruned(shortChannelId)) + primary.isPruned(shortChannelId) } } -case class DualAuditDb(sqlite: SqliteAuditDb, postgres: PgAuditDb) extends AuditDb { +case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-audit").build())) override def add(channelLifecycle: DbEventHandler.ChannelEvent): Unit = { - runAsync(postgres.add(channelLifecycle)) - sqlite.add(channelLifecycle) + runAsync(secondary.add(channelLifecycle)) + primary.add(channelLifecycle) } override def add(paymentSent: PaymentSent): Unit = { - runAsync(postgres.add(paymentSent)) - sqlite.add(paymentSent) + runAsync(secondary.add(paymentSent)) + primary.add(paymentSent) } override def add(paymentReceived: PaymentReceived): Unit = { - runAsync(postgres.add(paymentReceived)) - sqlite.add(paymentReceived) + runAsync(secondary.add(paymentReceived)) + primary.add(paymentReceived) } override def add(paymentRelayed: PaymentRelayed): Unit = { - runAsync(postgres.add(paymentRelayed)) - sqlite.add(paymentRelayed) + runAsync(secondary.add(paymentRelayed)) + primary.add(paymentRelayed) } override def add(txPublished: TransactionPublished): Unit = { - runAsync(postgres.add(txPublished)) - sqlite.add(txPublished) + runAsync(secondary.add(txPublished)) + primary.add(txPublished) } override def add(txConfirmed: TransactionConfirmed): Unit = { - runAsync(postgres.add(txConfirmed)) - sqlite.add(txConfirmed) + runAsync(secondary.add(txConfirmed)) + primary.add(txConfirmed) } override def add(channelErrorOccurred: ChannelErrorOccurred): Unit = { - runAsync(postgres.add(channelErrorOccurred)) - sqlite.add(channelErrorOccurred) + runAsync(secondary.add(channelErrorOccurred)) + primary.add(channelErrorOccurred) } override def addChannelUpdate(channelUpdateParametersChanged: ChannelUpdateParametersChanged): Unit = { - runAsync(postgres.addChannelUpdate(channelUpdateParametersChanged)) - sqlite.addChannelUpdate(channelUpdateParametersChanged) + runAsync(secondary.addChannelUpdate(channelUpdateParametersChanged)) + primary.addChannelUpdate(channelUpdateParametersChanged) } override def addPathFindingExperimentMetrics(metrics: PathFindingExperimentMetrics): Unit = { - runAsync(postgres.addPathFindingExperimentMetrics(metrics)) - sqlite.addPathFindingExperimentMetrics(metrics) + runAsync(secondary.addPathFindingExperimentMetrics(metrics)) + primary.addPathFindingExperimentMetrics(metrics) } override def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] = { - runAsync(postgres.listSent(from, to)) - sqlite.listSent(from, to) + runAsync(secondary.listSent(from, to)) + primary.listSent(from, to) } override def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] = { - runAsync(postgres.listReceived(from, to)) - sqlite.listReceived(from, to) + runAsync(secondary.listReceived(from, to)) + primary.listReceived(from, to) } override def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] = { - runAsync(postgres.listRelayed(from, to)) - sqlite.listRelayed(from, to) + runAsync(secondary.listRelayed(from, to)) + primary.listRelayed(from, to) } override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.NetworkFee] = { - runAsync(postgres.listNetworkFees(from, to)) - sqlite.listNetworkFees(from, to) + runAsync(secondary.listNetworkFees(from, to)) + primary.listNetworkFees(from, to) } override def stats(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.Stats] = { - runAsync(postgres.stats(from, to)) - sqlite.stats(from, to) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.stats(from, to)) + primary.stats(from, to) } } -case class DualChannelsDb(sqlite: SqliteChannelsDb, postgres: PgChannelsDb) extends ChannelsDb { +case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends ChannelsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-channels").build())) - override def addOrUpdateChannel(state: HasCommitments): Unit = { - runAsync(postgres.addOrUpdateChannel(state)) - sqlite.addOrUpdateChannel(state) + override def addOrUpdateChannel(data: PersistentChannelData): Unit = { + runAsync(secondary.addOrUpdateChannel(data)) + primary.addOrUpdateChannel(data) } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = { - runAsync(postgres.getChannel(channelId)) - sqlite.getChannel(channelId) + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = { + runAsync(secondary.getChannel(channelId)) + primary.getChannel(channelId) } override def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit = { - runAsync(postgres.updateChannelMeta(channelId, event)) - sqlite.updateChannelMeta(channelId, event) + runAsync(secondary.updateChannelMeta(channelId, event)) + primary.updateChannelMeta(channelId, event) } override def removeChannel(channelId: ByteVector32): Unit = { - runAsync(postgres.removeChannel(channelId)) - sqlite.removeChannel(channelId) + runAsync(secondary.removeChannel(channelId)) + primary.removeChannel(channelId) } - override def listLocalChannels(): Seq[HasCommitments] = { - runAsync(postgres.listLocalChannels()) - sqlite.listLocalChannels() + override def listLocalChannels(): Seq[PersistentChannelData] = { + runAsync(secondary.listLocalChannels()) + primary.listLocalChannels() } override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = { - runAsync(postgres.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)) - sqlite.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry) + runAsync(secondary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)) + primary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry) } override def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = { - runAsync(postgres.listHtlcInfos(channelId, commitmentNumber)) - sqlite.listHtlcInfos(channelId, commitmentNumber) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.listHtlcInfos(channelId, commitmentNumber)) + primary.listHtlcInfos(channelId, commitmentNumber) } } -case class DualPeersDb(sqlite: SqlitePeersDb, postgres: PgPeersDb) extends PeersDb { +case class DualPeersDb(primary: PeersDb, secondary: PeersDb) extends PeersDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-peers").build())) override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: NodeAddress): Unit = { - runAsync(postgres.addOrUpdatePeer(nodeId, address)) - sqlite.addOrUpdatePeer(nodeId, address) + runAsync(secondary.addOrUpdatePeer(nodeId, address)) + primary.addOrUpdatePeer(nodeId, address) } override def removePeer(nodeId: Crypto.PublicKey): Unit = { - runAsync(postgres.removePeer(nodeId)) - sqlite.removePeer(nodeId) + runAsync(secondary.removePeer(nodeId)) + primary.removePeer(nodeId) } override def getPeer(nodeId: Crypto.PublicKey): Option[NodeAddress] = { - runAsync(postgres.getPeer(nodeId)) - sqlite.getPeer(nodeId) + runAsync(secondary.getPeer(nodeId)) + primary.getPeer(nodeId) } override def listPeers(): Map[Crypto.PublicKey, NodeAddress] = { - runAsync(postgres.listPeers()) - sqlite.listPeers() + runAsync(secondary.listPeers()) + primary.listPeers() } override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { - runAsync(postgres.addOrUpdateRelayFees(nodeId, fees)) - sqlite.addOrUpdateRelayFees(nodeId, fees) + runAsync(secondary.addOrUpdateRelayFees(nodeId, fees)) + primary.addOrUpdateRelayFees(nodeId, fees) } override def getRelayFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { - runAsync(postgres.getRelayFees(nodeId)) - sqlite.getRelayFees(nodeId) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.getRelayFees(nodeId)) + primary.getRelayFees(nodeId) } } -case class DualPaymentsDb(sqlite: SqlitePaymentsDb, postgres: PgPaymentsDb) extends PaymentsDb { +case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends PaymentsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-payments").build())) override def listPaymentsOverview(limit: Int): Seq[PlainPayment] = { - runAsync(postgres.listPaymentsOverview(limit)) - sqlite.listPaymentsOverview(limit) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.listPaymentsOverview(limit)) + primary.listPaymentsOverview(limit) } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = { - runAsync(postgres.addIncomingPayment(pr, preimage, paymentType)) - sqlite.addIncomingPayment(pr, preimage, paymentType) + override def addIncomingPayment(pr: Invoice, preimage: ByteVector32, paymentType: String): Unit = { + runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) + primary.addIncomingPayment(pr, preimage, paymentType) } override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Boolean = { - runAsync(postgres.receiveIncomingPayment(paymentHash, amount, receivedAt)) - sqlite.receiveIncomingPayment(paymentHash, amount, receivedAt) + runAsync(secondary.receiveIncomingPayment(paymentHash, amount, receivedAt)) + primary.receiveIncomingPayment(paymentHash, amount, receivedAt) } override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { - runAsync(postgres.getIncomingPayment(paymentHash)) - sqlite.getIncomingPayment(paymentHash) + runAsync(secondary.getIncomingPayment(paymentHash)) + primary.getIncomingPayment(paymentHash) } override def removeIncomingPayment(paymentHash: ByteVector32): Try[Unit] = { - runAsync(postgres.removeIncomingPayment(paymentHash)) - sqlite.removeIncomingPayment(paymentHash) + runAsync(secondary.removeIncomingPayment(paymentHash)) + primary.removeIncomingPayment(paymentHash) } override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listIncomingPayments(from, to)) - sqlite.listIncomingPayments(from, to) + runAsync(secondary.listIncomingPayments(from, to)) + primary.listIncomingPayments(from, to) } override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listPendingIncomingPayments(from, to)) - sqlite.listPendingIncomingPayments(from, to) + runAsync(secondary.listPendingIncomingPayments(from, to)) + primary.listPendingIncomingPayments(from, to) } override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listExpiredIncomingPayments(from, to)) - sqlite.listExpiredIncomingPayments(from, to) + runAsync(secondary.listExpiredIncomingPayments(from, to)) + primary.listExpiredIncomingPayments(from, to) } override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listReceivedIncomingPayments(from, to)) - sqlite.listReceivedIncomingPayments(from, to) + runAsync(secondary.listReceivedIncomingPayments(from, to)) + primary.listReceivedIncomingPayments(from, to) } override def addOutgoingPayment(outgoingPayment: OutgoingPayment): Unit = { - runAsync(postgres.addOutgoingPayment(outgoingPayment)) - sqlite.addOutgoingPayment(outgoingPayment) + runAsync(secondary.addOutgoingPayment(outgoingPayment)) + primary.addOutgoingPayment(outgoingPayment) } override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = { - runAsync(postgres.updateOutgoingPayment(paymentResult)) - sqlite.updateOutgoingPayment(paymentResult) + runAsync(secondary.updateOutgoingPayment(paymentResult)) + primary.updateOutgoingPayment(paymentResult) } override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = { - runAsync(postgres.updateOutgoingPayment(paymentResult)) - sqlite.updateOutgoingPayment(paymentResult) + runAsync(secondary.updateOutgoingPayment(paymentResult)) + primary.updateOutgoingPayment(paymentResult) } override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { - runAsync(postgres.getOutgoingPayment(id)) - sqlite.getOutgoingPayment(id) + runAsync(secondary.getOutgoingPayment(id)) + primary.getOutgoingPayment(id) } override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(parentId)) - sqlite.listOutgoingPayments(parentId) + runAsync(secondary.listOutgoingPayments(parentId)) + primary.listOutgoingPayments(parentId) } override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(paymentHash)) - sqlite.listOutgoingPayments(paymentHash) + runAsync(secondary.listOutgoingPayments(paymentHash)) + primary.listOutgoingPayments(paymentHash) } override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(from, to)) - sqlite.listOutgoingPayments(from, to) + runAsync(secondary.listOutgoingPayments(from, to)) + primary.listOutgoingPayments(from, to) } - } -case class DualPendingCommandsDb(sqlite: SqlitePendingCommandsDb, postgres: PgPendingCommandsDb) extends PendingCommandsDb { +case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingCommandsDb) extends PendingCommandsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-pending-commands").build())) override def addSettlementCommand(channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit = { - runAsync(postgres.addSettlementCommand(channelId, cmd)) - sqlite.addSettlementCommand(channelId, cmd) + runAsync(secondary.addSettlementCommand(channelId, cmd)) + primary.addSettlementCommand(channelId, cmd) } override def removeSettlementCommand(channelId: ByteVector32, htlcId: Long): Unit = { - runAsync(postgres.removeSettlementCommand(channelId, htlcId)) - sqlite.removeSettlementCommand(channelId, htlcId) + runAsync(secondary.removeSettlementCommand(channelId, htlcId)) + primary.removeSettlementCommand(channelId, htlcId) } override def listSettlementCommands(channelId: ByteVector32): Seq[HtlcSettlementCommand] = { - runAsync(postgres.listSettlementCommands(channelId)) - sqlite.listSettlementCommands(channelId) + runAsync(secondary.listSettlementCommands(channelId)) + primary.listSettlementCommands(channelId) } override def listSettlementCommands(): Seq[(ByteVector32, HtlcSettlementCommand)] = { - runAsync(postgres.listSettlementCommands()) - sqlite.listSettlementCommands() - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.listSettlementCommands()) + primary.listSettlementCommands() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala deleted file mode 100644 index f8343eaef7..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.db - -import fr.acinq.eclair.blockchain.fee.FeeratesPerKB - -import java.io.Closeable - -/** - * This database stores the fee rates retrieved by a [[fr.acinq.eclair.blockchain.fee.FeeProvider]]. - */ -trait FeeratesDb extends Closeable { - - /** Insert or update the feerates into the feerates database. */ - def addOrUpdateFeerates(feeratesPerKB: FeeratesPerKB): Unit - - /** Return the (optional) feerates from the feerates database. */ - def getFeerates(): Option[FeeratesPerKB] - -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index b50815a024..ec3031fd0a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -16,16 +16,16 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} -import java.io.Closeable import scala.collection.immutable.SortedMap -trait NetworkDb extends Closeable { +trait NetworkDb { def addNode(n: NodeAnnouncement): Unit @@ -45,11 +45,11 @@ trait NetworkDb extends Closeable { def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit - def listChannels(): SortedMap[ShortChannelId, PublicChannel] + def listChannels(): SortedMap[RealShortChannelId, PublicChannel] - def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit + def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit - def removeFromPruned(shortChannelId: ShortChannelId): Unit + def removeFromPruned(shortChannelId: RealShortChannelId): Unit def isPruned(shortChannelId: ShortChannelId): Boolean diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala new file mode 100644 index 0000000000..727bf875d3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala @@ -0,0 +1,49 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.db + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} + +import java.util.UUID + +trait OffersDb extends OffersToSellDb with OffersToBuyDb + +trait OffersToSellDb { + + def addOfferToSell(offer: Offer): Unit + + def getOfferToSell(offerId: ByteVector32, pathId_opt: Option[ByteVector32]): Option[Offer] + + def listOffersToSell(): Seq[(Offer, Option[ByteVector32])] + + def addAttemptToSellOffer(offerId: ByteVector32, invoiceRequest: InvoiceRequest, invoice: Bolt12Invoice, preimage: ByteVector32, pathId: ByteVector32): Unit + +} + +trait OffersToBuyDb { + + def addAttemptToBuyOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, payerKey: PrivateKey, attemptId: UUID): Unit + + def getAttemptToBuyOffer(attemptId: UUID): Option[Any] + + def addInvoiceReceived(attemptId: UUID, invoice: Bolt12Invoice, paymentId_opt: Option[UUID]): Unit + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index fca1b86a66..615da166c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -16,26 +16,25 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} -import java.io.Closeable import java.util.UUID import scala.util.Try -trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with PaymentsOverviewDb with Closeable +trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with PaymentsOverviewDb trait IncomingPaymentsDb { /** Add a new expected incoming payment (not yet received). */ - def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit + def addIncomingPayment(pr: Invoice, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit /** - * Mark an incoming payment as received (paid). The received amount may exceed the payment request amount. - * If there was no matching payment request in the DB, this will return false. + * Mark an incoming payment as received (paid). The received amount may exceed the invoice amount. + * If there was no matching invoice in the DB, this will return false. */ def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli = TimestampMilli.now()): Boolean @@ -96,16 +95,16 @@ case object PaymentType { /** * An incoming payment received by this node. - * At first it is in a pending state once the payment request has been generated, then will become either a success (if - * we receive a valid HTLC) or a failure (if the payment request expires). + * At first it is in a pending state once the invoice has been generated, then will become either a success (if + * we receive a valid HTLC) or a failure (if the invoice expires). * - * @param paymentRequest Bolt 11 payment request. - * @param paymentPreimage pre-image associated with the payment request's payment_hash. + * @param invoice invoice. + * @param paymentPreimage pre-image associated with the invoice's payment_hash. * @param paymentType distinguish different payment types (standard, swaps, etc). - * @param createdAt absolute time in milli-seconds since UNIX epoch when the payment request was generated. + * @param createdAt absolute time in milli-seconds since UNIX epoch when the invoice was generated. * @param status current status of the payment. */ -case class IncomingPayment(paymentRequest: PaymentRequest, +case class IncomingPayment(invoice: Invoice, paymentPreimage: ByteVector32, paymentType: String, createdAt: TimestampMilli, @@ -124,7 +123,7 @@ object IncomingPaymentStatus { /** * Payment has been successfully received. * - * @param amount amount of the payment received, in milli-satoshis (may exceed the payment request amount). + * @param amount amount of the payment received, in milli-satoshis (may exceed the invoice amount). * @param receivedAt absolute time in milli-seconds since UNIX epoch when the payment was received. */ case class Received(amount: MilliSatoshi, receivedAt: TimestampMilli) extends IncomingPaymentStatus @@ -144,7 +143,7 @@ object IncomingPaymentStatus { * @param recipientAmount amount that will be received by the final recipient. * @param recipientNodeId id of the final recipient. * @param createdAt absolute time in milli-seconds since UNIX epoch when the payment was created. - * @param paymentRequest Bolt 11 payment request (if paying from an invoice). + * @param invoice invoice (if paying from an invoice). * @param status current status of the payment. */ case class OutgoingPayment(id: UUID, @@ -156,7 +155,7 @@ case class OutgoingPayment(id: UUID, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, createdAt: TimestampMilli, - paymentRequest: Option[PaymentRequest], + invoice: Option[Invoice], status: OutgoingPaymentStatus) sealed trait OutgoingPaymentStatus @@ -199,7 +198,7 @@ case class HopSummary(nodeId: PublicKey, nextNodeId: PublicKey, shortChannelId: object HopSummary { def apply(h: Hop): HopSummary = { val shortChannelId = h match { - case ChannelHop(_, _, channelUpdate) => Some(channelUpdate.shortChannelId) + case ch: ChannelHop => Some(ch.shortChannelId) case _: NodeHop => None } HopSummary(h.nodeId, h.nextNodeId, shortChannelId) @@ -231,7 +230,7 @@ trait PaymentsOverviewDb { } /** - * Generic payment trait holding only the minimum information in the most plain type possible. Notably, payment request + * Generic payment trait holding only the minimum information in the most plain type possible. Notably, the invoice * is kept as a String, because deserialization is costly. *

* This object should only be used for a high level snapshot of the payments stored in the payment database. @@ -241,7 +240,7 @@ trait PaymentsOverviewDb { sealed trait PlainPayment { val paymentHash: ByteVector32 val paymentType: String - val paymentRequest: Option[String] + val invoice: Option[String] val finalAmount: Option[MilliSatoshi] val createdAt: TimestampMilli val completedAt: Option[TimestampMilli] @@ -250,7 +249,7 @@ sealed trait PlainPayment { case class PlainIncomingPayment(paymentHash: ByteVector32, paymentType: String, finalAmount: Option[MilliSatoshi], - paymentRequest: Option[String], + invoice: Option[String], status: IncomingPaymentStatus, createdAt: TimestampMilli, completedAt: Option[TimestampMilli], @@ -261,7 +260,7 @@ case class PlainOutgoingPayment(parentId: Option[UUID], paymentHash: ByteVector32, paymentType: String, finalAmount: Option[MilliSatoshi], - paymentRequest: Option[String], + invoice: Option[String], status: OutgoingPaymentStatus, createdAt: TimestampMilli, completedAt: Option[TimestampMilli]) extends PlainPayment diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index ab979881fa..ea10f348e8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -16,13 +16,11 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol.NodeAddress -import java.io.Closeable - -trait PeersDb extends Closeable { +trait PeersDb { def addOrUpdatePeer(nodeId: PublicKey, address: NodeAddress): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala index 47bf04f163..240d209095 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala @@ -18,12 +18,10 @@ package fr.acinq.eclair.db import akka.actor.ActorRef import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.{UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc, UpdateMessage} -import java.io.Closeable - /** * This database stores CMD_FULFILL_HTLC and CMD_FAIL_HTLC that we have received from downstream * (either directly via UpdateFulfillHtlc or by extracting the value from the @@ -36,7 +34,7 @@ import java.io.Closeable * to handle all corner cases. * */ -trait PendingCommandsDb extends Closeable { +trait PendingCommandsDb { def addSettlementCommand(channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit @@ -56,7 +54,7 @@ object PendingCommandsDb { */ def safeSend(register: ActorRef, db: PendingCommandsDb, channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit = { // htlc settlement commands don't have replyTo - register ! Register.Forward(ActorRef.noSender, channelId, cmd) + register ! Register.Forward(null, channelId, cmd) // we store the command in a db (note that this happens *after* forwarding the command to the channel, so we don't add latency) db.addSettlementCommand(channelId, cmd) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala index dc08381680..b3da5d4595 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.jdbc -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.MilliSatoshi import grizzled.slf4j.Logger import org.sqlite.SQLiteConnection @@ -153,6 +153,11 @@ trait JdbcUtils { ByteVector.fromValidHex(s) } + def getByteVectorFromHexNullable(columnLabel: String): Option[ByteVector] = { + val s = rs.getString(columnLabel) + if (rs.wasNull()) None else Some(ByteVector.fromValidHex(s)) + } + def getByteVector32FromHex(columnLabel: String): ByteVector32 = { val s = rs.getString(columnLabel) ByteVector32(ByteVector.fromValidHex(s)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala new file mode 100644 index 0000000000..5d532e70a8 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala @@ -0,0 +1,280 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareAuditDb { + + private def compareSentTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "sent" + val table2 = "audit.sent" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + string(rs, "payment_id") ++ + string(rs, "parent_payment_id") ++ + bytes(rs, "payment_hash") ++ + bytes(rs, "payment_preimage") ++ + bytes(rs, "recipient_node_id") ++ + bytes(rs, "to_channel_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + string(rs, "payment_id") ++ + string(rs, "parent_payment_id") ++ + hex(rs, "payment_hash") ++ + hex(rs, "payment_preimage") ++ + hex(rs, "recipient_node_id") ++ + hex(rs, "to_channel_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareReceivedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "received" + val table2 = "audit.received" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + bytes(rs, "payment_hash") ++ + bytes(rs, "from_channel_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + hex(rs, "payment_hash") ++ + hex(rs, "from_channel_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relayed" + val table2 = "audit.relayed" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + bytes(rs, "channel_id") ++ + string(rs, "direction") ++ + string(rs, "relay_type") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + hex(rs, "channel_id") ++ + string(rs, "direction") ++ + string(rs, "relay_type") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayedTrampolineTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relayed_trampoline" + val table2 = "audit.relayed_trampoline" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + bytes(rs, "next_node_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + hex(rs, "next_node_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareTransactionsPublishedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "transactions_published" + val table2 = "audit.transactions_published" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "tx_id") ++ + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "mining_fee_sat") ++ + string(rs, "tx_type") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "tx_id") ++ + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "mining_fee_sat") ++ + string(rs, "tx_type") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareTransactionsConfirmedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "transactions_confirmed" + val table2 = "audit.transactions_confirmed" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "tx_id") ++ + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "tx_id") ++ + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelEventsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_events" + val table2 = "audit.channel_events" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "capacity_sat") ++ + bool(rs, "is_funder") ++ + bool(rs, "is_private") ++ + string(rs, "event") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "capacity_sat") ++ + bool(rs, "is_funder") ++ + bool(rs, "is_private") ++ + string(rs, "event") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelErrorsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_errors WHERE error_name <> 'CannotAffordFees'" + val table2 = "audit.channel_errors" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + string(rs, "error_name") ++ + string(rs, "error_message") ++ + bool(rs, "is_fatal") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + string(rs, "error_name") ++ + string(rs, "error_message") ++ + bool(rs, "is_fatal") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelUpdatesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_updates" + val table2 = "audit.channel_updates" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") ++ + long(rs, "cltv_expiry_delta") ++ + long(rs, "htlc_minimum_msat") ++ + long(rs, "htlc_maximum_msat") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") ++ + long(rs, "cltv_expiry_delta") ++ + long(rs, "htlc_minimum_msat") ++ + long(rs, "htlc_maximum_msat") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def comparePathFindingMetricsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "path_finding_metrics" + val table2 = "audit.path_finding_metrics" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + string(rs, "status") ++ + long(rs, "duration_ms") ++ + longts(rs, "timestamp") ++ + bool(rs, "is_mpp") ++ + string(rs, "experiment_name") ++ + bytes(rs, "recipient_node_id") + + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + string(rs, "status") ++ + long(rs, "duration_ms") ++ + ts(rs, "timestamp") ++ + bool(rs, "is_mpp") ++ + string(rs, "experiment_name") ++ + hex(rs, "recipient_node_id") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareSentTable(conn1, conn2) && + compareReceivedTable(conn1, conn2) && + compareRelayedTable(conn1, conn2) && + compareRelayedTrampolineTable(conn1, conn2) && + compareTransactionsPublishedTable(conn1, conn2) && + compareTransactionsConfirmedTable(conn1, conn2) && + compareChannelEventsTable(conn1, conn2) && + compareChannelErrorsTable(conn1, conn2) && + compareChannelUpdatesTable(conn1, conn2) && + comparePathFindingMetricsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala new file mode 100644 index 0000000000..3905503b0f --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala @@ -0,0 +1,80 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.channel.{DATA_CLOSING, DATA_WAIT_FOR_FUNDING_CONFIRMED} +import fr.acinq.eclair.db.migration.CompareDb._ +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareChannelsDb { + + private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "local_channels" + val table2 = "local.channels" + + def hash1(rs: ResultSet): ByteVector = { + val data = ByteVector(rs.getBytes("data")) + val data_modified = channelDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case _ => data + } + bytes(rs, "channel_id") ++ + data_modified ++ + bool(rs, "is_closed") ++ + longtsnull(rs, "created_timestamp") ++ + longtsnull(rs, "last_payment_sent_timestamp") ++ + longtsnull(rs, "last_payment_received_timestamp") ++ + longtsnull(rs, "last_connected_timestamp") ++ + longtsnull(rs, "closed_timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + val data = ByteVector(rs.getBytes("data")) + val data_modified = channelDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case _ => data + } + hex(rs, "channel_id") ++ + data_modified ++ + bool(rs, "is_closed") ++ + tsnull(rs, "created_timestamp") ++ + tsnull(rs, "last_payment_sent_timestamp") ++ + tsnull(rs, "last_payment_received_timestamp") ++ + tsnull(rs, "last_connected_timestamp") ++ + tsnull(rs, "closed_timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareHtlcInfosTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "htlc_infos" + val table2 = "local.htlc_infos" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + long(rs, "commitment_number") ++ + bytes(rs, "payment_hash") ++ + long(rs, "cltv_expiry") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + long(rs, "commitment_number") ++ + hex(rs, "payment_hash") ++ + long(rs, "cltv_expiry") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareChannelsTable(conn1, conn2) && + compareHtlcInfosTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala new file mode 100644 index 0000000000..d5efd6047a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala @@ -0,0 +1,81 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.DualDatabases +import fr.acinq.eclair.db.pg.PgUtils +import grizzled.slf4j.Logging +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareDb extends Logging { + + def compareTable(conn1: Connection, + conn2: Connection, + table1: String, + table2: String, + hash1: ResultSet => ByteVector, + hash2: ResultSet => ByteVector): Boolean = { + val rs1 = conn1.prepareStatement(s"SELECT * FROM $table1").executeQuery() + val rs2 = conn2.prepareStatement(s"SELECT * FROM $table2").executeQuery() + + var hashes1 = List.empty[ByteVector] + while (rs1.next()) { + hashes1 = hash1(rs1) +: hashes1 + } + + var hashes2 = List.empty[ByteVector] + while (rs2.next()) { + hashes2 = hash2(rs2) +: hashes2 + } + + val res = hashes1.sorted == hashes2.sorted + + if (res) { + logger.info(s"tables $table1/$table2 are identical") + } else { + val diff1 = hashes1 diff hashes2 + val diff2 = hashes2 diff hashes1 + logger.warn(s"tables $table1/$table2 are different diff1=${diff1.take(3).map(_.toHex.take(128))} diff2=${diff2.take(3).map(_.toHex.take(128))}") + } + + res + } + + // @formatter:off + import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ + def bytes(rs: ResultSet, columnName: String): ByteVector = rs.getByteVector(columnName) + def bytesnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) + def hex(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHex(columnName) + def hexnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHexNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) + def string(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getString(columnName).getBytes) + def stringnull(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getStringNullable(columnName).getOrElse("").getBytes) + def bool(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromByte(if (rs.getBoolean(columnName)) 1 else 0) + def long(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLong(columnName)) + def longnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).getOrElse(42)) + def longts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getLong(columnName).toDouble / 1_000_000).round) + def longtsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).map(l => (l.toDouble/1_000_000).round).getOrElse(42)) + def int(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromInt(rs.getInt(columnName)) + def ts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).getTime.toDouble / 1_000_000).round) + def tsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.getTime.toDouble / 1_000_000).round).getOrElse(42)) + def tssec(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).toInstant.getEpochSecond.toDouble / 1_000_000).round) + def tssecnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.toInstant.getEpochSecond.toDouble / 1_000_000).round).getOrElse(42)) + // @formatter:on + + def compareAll(dualDatabases: DualDatabases): Unit = { + logger.info("comparing all tables...") + val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) + PgUtils.inTransaction { postgres => + val result = List( + CompareChannelsDb.compareAllTables(sqliteDb.channels.sqlite, postgres), + ComparePendingCommandsDb.compareAllTables(sqliteDb.pendingCommands.sqlite, postgres), + ComparePeersDb.compareAllTables(sqliteDb.peers.sqlite, postgres), + ComparePaymentsDb.compareAllTables(sqliteDb.payments.sqlite, postgres), + CompareNetworkDb.compareAllTables(sqliteDb.network.sqlite, postgres), + CompareAuditDb.compareAllTables(sqliteDb.audit.sqlite, postgres) + ).forall(_ == true) + logger.info(s"comparison complete identical=$result") + }(postgresDb.dataSource) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala new file mode 100644 index 0000000000..8b91bb31d4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala @@ -0,0 +1,73 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareNetworkDb { + + private def compareNodesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "nodes" + val table2 = "network.nodes" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channels" + val table2 = "network.public_channels" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") ++ + string(rs, "txid") ++ + bytes(rs, "channel_announcement") ++ + long(rs, "capacity_sat") ++ + bytesnull(rs, "channel_update_1") ++ + bytesnull(rs, "channel_update_2") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") ++ + string(rs, "txid") ++ + bytes(rs, "channel_announcement") ++ + long(rs, "capacity_sat") ++ + bytesnull(rs, "channel_update_1") ++ + bytesnull(rs, "channel_update_2") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def comparePrunedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "pruned" + val table2 = "network.pruned_channels" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareNodesTable(conn1, conn2) && + compareChannelsTable(conn1, conn2) && + comparePrunedTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala new file mode 100644 index 0000000000..17265931c0 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala @@ -0,0 +1,87 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePaymentsDb { + + private def compareReceivedPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "received_payments" + val table2 = "payments.received" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + string(rs, "payment_type") ++ + bytes(rs, "payment_preimage") ++ + string(rs, "payment_request") ++ + longnull(rs, "received_msat") ++ + longts(rs, "created_at") ++ + longts(rs, "expire_at") ++ + longtsnull(rs, "received_at") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + string(rs, "payment_type") ++ + hex(rs, "payment_preimage") ++ + string(rs, "payment_request") ++ + longnull(rs, "received_msat") ++ + ts(rs, "created_at") ++ + ts(rs, "expire_at") ++ + tsnull(rs, "received_at") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareSentPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "sent_payments" + val table2 = "payments.sent" + + def hash1(rs: ResultSet): ByteVector = { + string(rs, "id") ++ + string(rs, "parent_id") ++ + stringnull(rs, "external_id") ++ + bytes(rs, "payment_hash") ++ + bytesnull(rs, "payment_preimage") ++ + string(rs, "payment_type") ++ + long(rs, "amount_msat") ++ + longnull(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + bytes(rs, "recipient_node_id") ++ + stringnull(rs, "payment_request") ++ + bytesnull(rs, "payment_route") ++ + bytesnull(rs, "failures") ++ + longts(rs, "created_at") ++ + longtsnull(rs, "completed_at") + } + + def hash2(rs: ResultSet): ByteVector = { + string(rs, "id") ++ + string(rs, "parent_id") ++ + stringnull(rs, "external_id") ++ + hex(rs, "payment_hash") ++ + hexnull(rs, "payment_preimage") ++ + string(rs, "payment_type") ++ + long(rs, "amount_msat") ++ + longnull(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + hex(rs, "recipient_node_id") ++ + stringnull(rs, "payment_request") ++ + bytesnull(rs, "payment_route") ++ + bytesnull(rs, "failures") ++ + ts(rs, "created_at") ++ + tsnull(rs, "completed_at") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareReceivedPaymentsTable(conn1, conn2) && + compareSentPaymentsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala new file mode 100644 index 0000000000..c0c034fa97 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala @@ -0,0 +1,51 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePeersDb { + + private def comparePeersTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "peers" + val table2 = "local.peers" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayFeesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relay_fees" + val table2 = "local.relay_fees" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + comparePeersTable(conn1, conn2) && + compareRelayFeesTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala new file mode 100644 index 0000000000..eb099ceea4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala @@ -0,0 +1,33 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePendingCommandsDb { + + private def comparePendingSettlementCommandsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "pending_settlement_commands" + val table2 = "local.pending_settlement_commands" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + long(rs, "htlc_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + long(rs, "htlc_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + comparePendingSettlementCommandsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala new file mode 100644 index 0000000000..28705cd272 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala @@ -0,0 +1,188 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigrateAuditDb { + + private def migrateSentTable(source: Connection, destination: Connection): Int = { + val sourceTable = "sent" + val insertSql = "INSERT INTO audit.sent (amount_msat, fees_msat, recipient_amount_msat, payment_id, parent_payment_id, payment_hash, payment_preimage, recipient_node_id, to_channel_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setLong(2, rs.getLong("fees_msat")) + insertStatement.setLong(3, rs.getLong("recipient_amount_msat")) + insertStatement.setString(4, rs.getString("payment_id")) + insertStatement.setString(5, rs.getString("parent_payment_id")) + insertStatement.setString(6, rs.getByteVector32("payment_hash").toHex) + insertStatement.setString(7, rs.getByteVector32("payment_preimage").toHex) + insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) + insertStatement.setString(9, rs.getByteVector32("to_channel_id").toHex) + insertStatement.setTimestamp(10, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateReceivedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "received" + val insertSql = "INSERT INTO audit.received (amount_msat, payment_hash, from_channel_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setString(2, rs.getByteVector32("payment_hash").toHex) + insertStatement.setString(3, rs.getByteVector32("from_channel_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relayed" + val insertSql = "INSERT INTO audit.relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(2, rs.getLong("amount_msat")) + insertStatement.setString(3, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(4, rs.getString("direction")) + insertStatement.setString(5, rs.getString("relay_type")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayedTrampolineTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relayed_trampoline" + val insertSql = "INSERT INTO audit.relayed_trampoline (payment_hash, amount_msat, next_node_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(2, rs.getLong("amount_msat")) + insertStatement.setString(3, rs.getByteVector("next_node_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateTransactionsPublishedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "transactions_published" + val insertSql = "INSERT INTO audit.transactions_published (tx_id, channel_id, node_id, mining_fee_sat, tx_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) + insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(3, rs.getByteVector("node_id").toHex) + insertStatement.setLong(4, rs.getLong("mining_fee_sat")) + insertStatement.setString(5, rs.getString("tx_type")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateTransactionsConfirmedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "transactions_confirmed" + val insertSql = "INSERT INTO audit.transactions_confirmed (tx_id, channel_id, node_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) + insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(3, rs.getByteVector("node_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelEventsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_events" + val insertSql = "INSERT INTO audit.channel_events (channel_id, node_id, capacity_sat, is_funder, is_private, event, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setLong(3, rs.getLong("capacity_sat")) + insertStatement.setBoolean(4, rs.getBoolean("is_funder")) + insertStatement.setBoolean(5, rs.getBoolean("is_private")) + insertStatement.setString(6, rs.getString("event")) + insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelErrorsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_errors WHERE error_name <> 'CannotAffordFees'" + val insertSql = "INSERT INTO audit.channel_errors (channel_id, node_id, error_name, error_message, is_fatal, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setString(3, rs.getString("error_name")) + insertStatement.setString(4, rs.getString("error_message")) + insertStatement.setBoolean(5, rs.getBoolean("is_fatal")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelUpdatesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_updates" + val insertSql = "INSERT INTO audit.channel_updates (channel_id, node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta, htlc_minimum_msat, htlc_maximum_msat, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setLong(3, rs.getLong("fee_base_msat")) + insertStatement.setLong(4, rs.getLong("fee_proportional_millionths")) + insertStatement.setLong(5, rs.getLong("cltv_expiry_delta")) + insertStatement.setLong(6, rs.getLong("htlc_minimum_msat")) + insertStatement.setLong(7, rs.getLong("htlc_maximum_msat")) + insertStatement.setTimestamp(8, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migratePathFindingMetricsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "path_finding_metrics" + val insertSql = "INSERT INTO audit.path_finding_metrics (amount_msat, fees_msat, status, duration_ms, timestamp, is_mpp, experiment_name, recipient_node_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setLong(2, rs.getLong("fees_msat")) + insertStatement.setString(3, rs.getString("status")) + insertStatement.setLong(4, rs.getLong("duration_ms")) + insertStatement.setTimestamp(5, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + insertStatement.setBoolean(6, rs.getBoolean("is_mpp")) + insertStatement.setString(7, rs.getString("experiment_name")) + insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "audit", 8, 10) + migrateSentTable(source, destination) + migrateReceivedTable(source, destination) + migrateRelayedTable(source, destination) + migrateRelayedTrampolineTable(source, destination) + migrateTransactionsPublishedTable(source, destination) + migrateTransactionsConfirmedTable(source, destination) + migrateChannelEventsTable(source, destination) + migrateChannelErrorsTable(source, destination) + migrateChannelUpdatesTable(source, destination) + migratePathFindingMetricsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala new file mode 100644 index 0000000000..2b4165daea --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala @@ -0,0 +1,56 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec +import scodec.bits.BitVector + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigrateChannelsDb { + + private def migrateChannelsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "local_channels" + val insertSql = "INSERT INTO local.channels (channel_id, data, json, is_closed, created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp, closed_timestamp) VALUES (?, ?, ?::JSONB, ?, ?, ?, ?, ?, ?)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val json = serialization.write(state) + insertStatement.setString(3, json) + insertStatement.setBoolean(4, rs.getBoolean("is_closed")) + insertStatement.setTimestamp(5, rs.getLongNullable("created_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(6, rs.getLongNullable("last_payment_sent_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(7, rs.getLongNullable("last_payment_received_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(8, rs.getLongNullable("last_connected_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(9, rs.getLongNullable("closed_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateHtlcInfos(source: Connection, destination: Connection): Int = { + val sourceTable = "htlc_infos" + val insertSql = "INSERT INTO local.htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setLong(2, rs.getLong("commitment_number")) + insertStatement.setString(3, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(4, rs.getLong("cltv_expiry")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "channels", 4, 7) + migrateChannelsTable(source, destination) + migrateHtlcInfos(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala new file mode 100644 index 0000000000..331e62b1da --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala @@ -0,0 +1,53 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.DualDatabases +import fr.acinq.eclair.db.jdbc.JdbcUtils +import fr.acinq.eclair.db.pg.PgUtils +import grizzled.slf4j.Logging + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigrateDb extends Logging { + + private def getVersion(conn: Connection, + dbName: String): Int = { + val statement = conn.prepareStatement(s"SELECT version FROM versions WHERE db_name='$dbName'") + val res = statement.executeQuery() + res.next() + res.getInt("version") + } + + def checkVersions(source: Connection, + destination: Connection, + dbName: String, + expectedSourceVersion: Int, + expectedDestinationVersion: Int): Unit = { + val actualSourceVersion = getVersion(source, dbName) + val actualDestinationVersion = getVersion(destination, dbName) + require(actualSourceVersion == expectedSourceVersion, s"unexpected version for source db=$dbName expected=$expectedSourceVersion actual=$actualSourceVersion") + require(actualDestinationVersion == expectedDestinationVersion, s"unexpected version for destination db=$dbName expected=$expectedDestinationVersion actual=$actualDestinationVersion") + } + + def migrateTable(source: Connection, + destination: Connection, + sourceTable: String, + insertSql: String, + migrate: (ResultSet, PreparedStatement) => Unit): Int = + JdbcUtils.migrateTable(source, destination, sourceTable, insertSql, migrate)(logger) + + def migrateAll(dualDatabases: DualDatabases): Unit = { + logger.info("migrating all tables...") + val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) + PgUtils.inTransaction { postgres => + MigrateChannelsDb.migrateAllTables(sqliteDb.channels.sqlite, postgres) + MigratePendingCommandsDb.migrateAllTables(sqliteDb.pendingCommands.sqlite, postgres) + MigratePeersDb.migrateAllTables(sqliteDb.peers.sqlite, postgres) + MigratePaymentsDb.migrateAllTables(sqliteDb.payments.sqlite, postgres) + MigrateNetworkDb.migrateAllTables(sqliteDb.network.sqlite, postgres) + MigrateAuditDb.migrateAllTables(sqliteDb.audit.sqlite, postgres) + logger.info("migration complete") + }(postgresDb.dataSource) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala new file mode 100644 index 0000000000..0f2b4ccdf8 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala @@ -0,0 +1,74 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} +import scodec.bits.BitVector + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigrateNetworkDb { + + private def migrateNodesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "nodes" + val insertSql = "INSERT INTO network.nodes (node_id, data, json) VALUES (?, ?, ?::JSONB)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + val state = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value + val json = serialization.write(state) + insertStatement.setString(3, json) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channels" + val insertSql = "INSERT INTO network.public_channels (short_channel_id, txid, channel_announcement, capacity_sat, channel_update_1, channel_update_2, channel_announcement_json, channel_update_1_json, channel_update_2_json) VALUES (?, ?, ?, ?, ?, ?, ?::JSONB, ?::JSONB, ?::JSONB)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("short_channel_id")) + insertStatement.setString(2, rs.getString("txid")) + insertStatement.setBytes(3, rs.getBytes("channel_announcement")) + insertStatement.setLong(4, rs.getLong("capacity_sat")) + insertStatement.setBytes(5, rs.getBytes("channel_update_1")) + insertStatement.setBytes(6, rs.getBytes("channel_update_2")) + val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value + val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) + val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) + val json = serialization.write(ann) + val u1_json = channel_update_1_opt.map(serialization.write(_)).orNull + val u2_json = channel_update_2_opt.map(serialization.write(_)).orNull + insertStatement.setString(7, json) + insertStatement.setString(8, u1_json) + insertStatement.setString(9, u2_json) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migratePrunedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "pruned" + val insertSql = "INSERT INTO network.pruned_channels (short_channel_id) VALUES (?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("short_channel_id")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "network", 2, 4) + migrateNodesTable(source, destination) + migrateChannelsTable(source, destination) + migratePrunedTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala new file mode 100644 index 0000000000..ac2e885bf3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala @@ -0,0 +1,60 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigratePaymentsDb { + + private def migrateReceivedPaymentsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "received_payments" + val insertSql = "INSERT INTO payments.received (payment_hash, payment_type, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("payment_hash").toHex) + insertStatement.setString(2, rs.getString("payment_type")) + insertStatement.setString(3, rs.getByteVector("payment_preimage").toHex) + insertStatement.setString(4, rs.getString("payment_request")) + insertStatement.setObject(5, rs.getLongNullable("received_msat").orNull) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) + insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("expire_at")))) + insertStatement.setObject(8, rs.getLongNullable("received_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateSentPaymentsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "sent_payments" + val insertSql = "INSERT INTO payments.sent (id, parent_id, external_id, payment_hash, payment_preimage, payment_type, amount_msat, fees_msat, recipient_amount_msat, recipient_node_id, payment_request, payment_route, failures, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getString("id")) + insertStatement.setString(2, rs.getString("parent_id")) + insertStatement.setString(3, rs.getStringNullable("external_id").orNull) + insertStatement.setString(4, rs.getByteVector("payment_hash").toHex) + insertStatement.setString(5, rs.getByteVector32Nullable("payment_preimage").map(_.toHex).orNull) + insertStatement.setString(6, rs.getString("payment_type")) + insertStatement.setLong(7, rs.getLong("amount_msat")) + insertStatement.setObject(8, rs.getLongNullable("fees_msat").orNull) + insertStatement.setLong(9, rs.getLong("recipient_amount_msat")) + insertStatement.setString(10, rs.getByteVector("recipient_node_id").toHex) + insertStatement.setString(11, rs.getStringNullable("payment_request").orNull) + insertStatement.setBytes(12, rs.getBytes("payment_route")) + insertStatement.setBytes(13, rs.getBytes("failures")) + insertStatement.setTimestamp(14, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) + insertStatement.setObject(15, rs.getLongNullable("completed_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "payments", 4, 6) + migrateReceivedPaymentsTable(source, destination) + migrateSentPaymentsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala new file mode 100644 index 0000000000..77021f3a3d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala @@ -0,0 +1,41 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigratePeersDb { + + private def migratePeersTable(source: Connection, destination: Connection): Int = { + val sourceTable = "peers" + val insertSql = "INSERT INTO local.peers (node_id, data) VALUES (?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayFeesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relay_fees" + val insertSql = "INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) VALUES (?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setLong(2, rs.getLong("fee_base_msat")) + insertStatement.setLong(3, rs.getLong("fee_proportional_millionths")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "peers", 2, 3) + migratePeersTable(source, destination) + migrateRelayFeesTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala new file mode 100644 index 0000000000..6a01208766 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala @@ -0,0 +1,28 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigratePendingCommandsDb { + + private def migratePendingSettlementCommandsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "pending_settlement_commands" + val insertSql = "INSERT INTO local.pending_settlement_commands (channel_id, htlc_id, data) VALUES (?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("channel_id").toHex) + insertStatement.setLong(2, rs.getLong("htlc_id")) + insertStatement.setBytes(3, rs.getBytes("data")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "pending_relay", 2, 3) + migratePendingSettlementCommandsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 0230714b1d..4305217916 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -36,7 +36,7 @@ import javax.sql.DataSource object PgAuditDb { val DB_NAME = "audit" - val CURRENT_VERSION = 10 + val CURRENT_VERSION = 11 } class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { @@ -44,6 +44,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { import PgUtils._ import ExtendedResultSet._ import PgAuditDb._ + import fr.acinq.eclair.json.JsonSerializers.{formats, serialization} case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: TimestampMilli) @@ -102,6 +103,13 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("DROP TABLE audit.network_fees") } + def migration1011(statement: Statement): Unit = { + statement.executeUpdate("ALTER TABLE audit.path_finding_metrics ADD COLUMN payment_hash TEXT") + statement.executeUpdate("ALTER TABLE audit.path_finding_metrics ADD COLUMN routing_hints JSONB") + statement.executeUpdate("CREATE INDEX metrics_hash_idx ON audit.path_finding_metrics(payment_hash)") + statement.executeUpdate("CREATE INDEX metrics_recipient_idx ON audit.path_finding_metrics(recipient_node_id)") + } + getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA audit") @@ -112,7 +120,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") - statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL, payment_hash TEXT, routing_hints JSONB)") statement.executeUpdate("CREATE TABLE audit.transactions_published (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, mining_fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.transactions_confirmed (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") @@ -132,9 +140,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX metrics_timestamp_idx ON audit.path_finding_metrics(timestamp)") statement.executeUpdate("CREATE INDEX metrics_mpp_idx ON audit.path_finding_metrics(is_mpp)") statement.executeUpdate("CREATE INDEX metrics_name_idx ON audit.path_finding_metrics(experiment_name)") + statement.executeUpdate("CREATE INDEX metrics_recipient_idx ON audit.path_finding_metrics(recipient_node_id)") + statement.executeUpdate("CREATE INDEX metrics_hash_idx ON audit.path_finding_metrics(payment_hash)") statement.executeUpdate("CREATE INDEX transactions_published_timestamp_idx ON audit.transactions_published(timestamp)") statement.executeUpdate("CREATE INDEX transactions_confirmed_timestamp_idx ON audit.transactions_confirmed(timestamp)") - case Some(v@(4 | 5 | 6 | 7 | 8 | 9)) => + case Some(v@(4 | 5 | 6 | 7 | 8 | 9 | 10)) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") if (v < 5) { migration45(statement) @@ -154,6 +164,9 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { if (v < 10) { migration910(statement) } + if (v < 11) { + migration1011(statement) + } case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -167,7 +180,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(1, e.channelId.toHex) statement.setString(2, e.remoteNodeId.value.toHex) statement.setLong(3, e.capacity.toLong) - statement.setBoolean(4, e.isFunder) + statement.setBoolean(4, e.isInitiator) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event.label) statement.setTimestamp(7, Timestamp.from(Instant.now())) @@ -305,7 +318,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { override def addPathFindingExperimentMetrics(m: PathFindingExperimentMetrics): Unit = withMetrics("audit/add-experiment-metrics", DbBackends.Postgres) { inTransaction { pg => - using(pg.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + using(pg.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?::JSONB)")) { statement => statement.setLong(1, m.amount.toLong) statement.setLong(2, m.fees.toLong) statement.setString(3, m.status) @@ -314,6 +327,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setBoolean(6, m.isMultiPart) statement.setString(7, m.experimentName) statement.setString(8, m.recipientNodeId.value.toHex) + statement.setString(9, m.paymentHash.toHex) + statement.setString(10, m.routingHints_opt.map(serialization.write(_)).orNull) statement.executeUpdate() } } @@ -474,7 +489,4 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } }) } - - override def close(): Unit = () - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 1d6499a9c9..4b003da8a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -17,15 +17,15 @@ package fr.acinq.eclair.db.pg import com.zaxxer.hikari.util.IsolationLevel -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -89,9 +89,9 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit (rs, statement) => { // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't // store local commitment signatures anymore, and we want to clean up existing data - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = stateDataCodec.encode(state).require.toByteArray - val json = serialization.writePretty(state) + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val data = channelDataCodec.encode(state).require.toByteArray + val json = serialization.write(state) statement.setBytes(1, data) statement.setString(2, json) statement.setString(3, state.channelId.toHex) @@ -140,17 +140,17 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit table, s"UPDATE $table SET json=?::JSONB WHERE channel_id=?", (rs, statement) => { - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(state) + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val json = serialization.write(state) statement.setString(1, json) statement.setString(2, state.channelId.toHex) } )(logger) } - override def addOrUpdateChannel(state: HasCommitments): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Postgres) { + override def addOrUpdateChannel(data: PersistentChannelData): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Postgres) { withLock { pg => - val data = stateDataCodec.encode(state).require.toByteArray + val encoded = channelDataCodec.encode(data).require.toByteArray using(pg.prepareStatement( """ | INSERT INTO local.channels (channel_id, data, json, is_closed) @@ -158,19 +158,19 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit | ON CONFLICT (channel_id) | DO UPDATE SET data = EXCLUDED.data, json = EXCLUDED.json ; | """.stripMargin)) { statement => - statement.setString(1, state.channelId.toHex) - statement.setBytes(2, data) - statement.setString(3, serialization.writePretty(state)) + statement.setString(1, data.channelId.toHex) + statement.setBytes(2, encoded) + statement.setString(3, serialization.write(data)) statement.executeUpdate() } } } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Postgres) { + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT data FROM local.channels WHERE channel_id=? AND is_closed=FALSE")) { statement => statement.setString(1, channelId.toHex) - statement.executeQuery.mapCodec(stateDataCodec).lastOption + statement.executeQuery.mapCodec(channelDataCodec).lastOption } } } @@ -219,11 +219,11 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit } } - override def listLocalChannels(): Seq[HasCommitments] = withMetrics("channels/list-local-channels", DbBackends.Postgres) { + override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Postgres) { withLock { pg => using(pg.createStatement) { statement => statement.executeQuery("SELECT data FROM local.channels WHERE is_closed=FALSE") - .mapCodec(stateDataCodec).toSeq + .mapCodec(channelDataCodec).toSeq } } } @@ -252,6 +252,4 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index 426f6bb1c8..c44ca7c892 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -92,24 +93,24 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { val channelsTable = if (oldTableName) "channels" else "network.public_channels" migrateTable(connection, connection, nodesTable, - s"UPDATE $nodesTable SET json=?::JSON WHERE node_id=?", + s"UPDATE $nodesTable SET json=?::JSONB WHERE node_id=?", (rs, statement) => { val node = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(node) + val json = serialization.write(node) statement.setString(1, json) statement.setString(2, node.nodeId.toString()) } )(logger) migrateTable(connection, connection, channelsTable, - s"UPDATE $channelsTable SET channel_announcement_json=?::JSON, channel_update_1_json=?::JSON, channel_update_2_json=?::JSON WHERE short_channel_id=?", + s"UPDATE $channelsTable SET channel_announcement_json=?::JSONB, channel_update_1_json=?::JSONB, channel_update_2_json=?::JSONB WHERE short_channel_id=?", (rs, statement) => { val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) - val json = serialization.writePretty(ann) - val u1_json = channel_update_1_opt.map(serialization.writePretty(_)).orNull - val u2_json = channel_update_2_opt.map(serialization.writePretty(_)).orNull + val json = serialization.write(ann) + val u1_json = channel_update_1_opt.map(serialization.write(_)).orNull + val u2_json = channel_update_2_opt.map(serialization.write(_)).orNull statement.setString(1, json) statement.setString(2, u1_json) statement.setString(3, u2_json) @@ -124,7 +125,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { statement => statement.setString(1, n.nodeId.value.toHex) statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray) - statement.setString(3, serialization.writePretty(n)) + statement.setString(3, serialization.write(n)) statement.executeUpdate() } } @@ -135,7 +136,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { using(pg.prepareStatement("UPDATE network.nodes SET data=?, json=?::JSONB WHERE node_id=?")) { statement => statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray) - statement.setString(2, serialization.writePretty(n)) + statement.setString(2, serialization.write(n)) statement.setString(3, n.nodeId.value.toHex) statement.executeUpdate() } @@ -180,7 +181,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { statement.setString(2, txid.toHex) statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray) statement.setLong(4, capacity.toLong) - statement.setString(5, serialization.writePretty(c)) + statement.setString(5, serialization.write(c)) statement.executeUpdate() } } @@ -192,18 +193,18 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { using(pg.prepareStatement(s"UPDATE network.public_channels SET $column=?, ${column}_json=?::JSONB WHERE short_channel_id=?")) { statement => statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) - statement.setString(2, serialization.writePretty(u)) + statement.setString(2, serialization.write(u)) statement.setLong(3, u.shortChannelId.toLong) statement.executeUpdate() } } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { inTransaction { pg => using(pg.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM network.public_channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -223,12 +224,13 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { })")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } @@ -236,7 +238,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement("INSERT INTO network.pruned_channels VALUES (?) ON CONFLICT DO NOTHING")) { statement => @@ -249,7 +251,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement(s"DELETE FROM network.pruned_channels WHERE short_channel_id=?")) { statement => @@ -267,6 +269,4 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index b50d3656a4..9afcac83bc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.payment.{Invoice, PaymentFailed, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -32,7 +32,6 @@ import java.sql.{Connection, ResultSet, Statement, Timestamp} import java.time.Instant import java.util.UUID import javax.sql.DataSource -import scala.concurrent.duration.DurationLong import scala.util.{Failure, Success, Try} object PgPaymentsDb { @@ -106,7 +105,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) statement.setTimestamp(9, sent.createdAt.toSqlTimestamp) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) statement.executeUpdate() } } @@ -157,7 +156,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")), TimestampMilli(rs.getTimestamp("created_at").getTime), - rs.getStringNullable("payment_request").map(PaymentRequest.read), + rs.getStringNullable("payment_request").map(Invoice.fromString(_).get), status ) } @@ -220,15 +219,15 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { + override def addIncomingPayment(invoice: Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("INSERT INTO payments.received (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, pr.paymentHash.toHex) + statement.setString(1, invoice.paymentHash.toHex) statement.setString(2, preimage.toHex) statement.setString(3, paymentType) - statement.setString(4, PaymentRequest.write(pr)) - statement.setTimestamp(5, pr.timestamp.toSqlTimestamp) - statement.setTimestamp(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS).seconds).toSqlTimestamp) + statement.setString(4, invoice.toString) + statement.setTimestamp(5, invoice.createdAt.toSqlTimestamp) + statement.setTimestamp(6, (invoice.createdAt + invoice.relativeExpiry.toSeconds).toSqlTimestamp) statement.executeUpdate() } } @@ -247,19 +246,19 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val paymentRequest = rs.getString("payment_request") + val invoice = Invoice.fromString(rs.getString("payment_request")).get IncomingPayment( - PaymentRequest.read(paymentRequest), + invoice, rs.getByteVector32FromHex("payment_preimage"), rs.getString("payment_type"), TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], invoice_opt: Option[Invoice], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired + case None if invoice_opt.exists(_.isExpired()) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -388,26 +387,23 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit val externalId_opt = rs.getStringNullable("external_id") val paymentHash = rs.getByteVector32FromHex("payment_hash") val paymentType = rs.getString("payment_type") - val paymentRequest_opt = rs.getStringNullable("payment_request") + val invoice_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") val createdAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")) val completedAt_opt = rs.getTimestampNullable("completed_at").map(TimestampMilli.fromSqlTimestamp) val expireAt_opt = rs.getTimestampNullable("expire_at").map(TimestampMilli.fromSqlTimestamp) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) - PlainIncomingPayment(paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt.map(Invoice.fromString(_).get), completedAt_opt) + PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") // note that the resulting status will not contain any details (routes, failures...) val status: OutgoingPaymentStatus = buildOutgoingPaymentStatus(preimage_opt, None, None, completedAt_opt, None) - PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt) + PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt) } }.toSeq } } } - - override def close(): Unit = () - } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 9fddafb70e..45094dd4a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends @@ -155,6 +155,4 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala index 80b9179c42..2ebd7d57b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.HtlcSettlementCommand import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends @@ -113,6 +113,4 @@ class PgPendingCommandsDb(implicit ds: DataSource, lock: PgLock) extends Pending } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala index 23d662c5d9..bea3a81171 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala @@ -32,9 +32,6 @@ import scala.concurrent.duration._ object PgUtils extends JdbcUtils { - /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ - case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`") - sealed trait PgLock { def obtainExclusiveLock(implicit ds: DataSource): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 74c01a145e..94fc3b1a5d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -37,7 +37,7 @@ object SqliteAuditDb { val CURRENT_VERSION = 8 } -class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { +class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { import SqliteUtils._ import ExtendedResultSet._ @@ -174,7 +174,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.remoteNodeId.value.toArray) statement.setLong(3, e.capacity.toLong) - statement.setBoolean(4, e.isFunder) + statement.setBoolean(4, e.isInitiator) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event.label) statement.setLong(7, TimestampMilli.now().toLong) @@ -460,8 +460,4 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } }) } - - // used by mobile apps - override def close(): Unit = sqlite.close() - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index b7dfcad1ea..c39ee4f68d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.{CltvExpiry, TimestampMilli} -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec +import fr.acinq.eclair.{CltvExpiry, TimestampMilli} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -70,8 +70,8 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { (rs, statement) => { // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't // store local commitment signatures anymore, and we want to clean up existing data - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = stateDataCodec.encode(state).require.toByteArray + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val data = channelDataCodec.encode(state).require.toByteArray statement.setBytes(1, data) statement.setBytes(2, state.channelId.toArray) } @@ -100,25 +100,25 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { setVersion(statement, DB_NAME, CURRENT_VERSION) } - override def addOrUpdateChannel(state: HasCommitments): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Sqlite) { - val data = stateDataCodec.encode(state).require.toByteArray + override def addOrUpdateChannel(data: PersistentChannelData): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Sqlite) { + val encoded = channelDataCodec.encode(data).require.toByteArray using(sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => - update.setBytes(1, data) - update.setBytes(2, state.channelId.toArray) + update.setBytes(1, encoded) + update.setBytes(2, data.channelId.toArray) if (update.executeUpdate() == 0) { using(sqlite.prepareStatement("INSERT INTO local_channels (channel_id, data, is_closed) VALUES (?, ?, 0)")) { statement => - statement.setBytes(1, state.channelId.toArray) - statement.setBytes(2, data) + statement.setBytes(1, data.channelId.toArray) + statement.setBytes(2, encoded) statement.executeUpdate() } } } } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Sqlite) { + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT data FROM local_channels WHERE channel_id=? AND is_closed=0")) { statement => statement.setBytes(1, channelId.toArray) - statement.executeQuery.mapCodec(stateDataCodec).lastOption + statement.executeQuery.mapCodec(channelDataCodec).lastOption } } @@ -162,10 +162,10 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { } } - override def listLocalChannels(): Seq[HasCommitments] = withMetrics("channels/list-local-channels", DbBackends.Sqlite) { + override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Sqlite) { using(sqlite.createStatement) { statement => statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") - .mapCodec(stateDataCodec).toSeq + .mapCodec(channelDataCodec).toSeq } } @@ -188,7 +188,4 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { .toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala deleted file mode 100644 index 3717fa5bea..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.db.sqlite - -import fr.acinq.bitcoin.Satoshi -import fr.acinq.eclair.TimestampMilli -import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} -import fr.acinq.eclair.db.FeeratesDb -import grizzled.slf4j.Logging - -import java.sql.{Connection, Statement} - -object SqliteFeeratesDb { - val DB_NAME = "feerates" - val CURRENT_VERSION = 2 -} - -class SqliteFeeratesDb(sqlite: Connection) extends FeeratesDb with Logging { - - import SqliteFeeratesDb._ - import SqliteUtils.ExtendedResultSet._ - import SqliteUtils._ - - using(sqlite.createStatement(), inTransaction = true) { statement => - - def migration12(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE feerates_per_kb RENAME TO _feerates_per_kb_old") - statement.executeUpdate( - """ - |CREATE TABLE feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, rate_blocks_1008 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - statement.executeUpdate("INSERT INTO feerates_per_kb (rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_1008, timestamp) SELECT rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_144, timestamp FROM _feerates_per_kb_old") - statement.executeUpdate("DROP table _feerates_per_kb_old") - } - - getVersion(statement, DB_NAME) match { - case None => - // Create feerates table. Rates are in kb. - statement.executeUpdate( - """ - |CREATE TABLE feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, rate_blocks_1008 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - case Some(v@1) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration12(statement) - case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do - case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") - } - setVersion(statement, DB_NAME, CURRENT_VERSION) - } - - override def addOrUpdateFeerates(feeratesPerKB: FeeratesPerKB): Unit = { - using(sqlite.prepareStatement("UPDATE feerates_per_kb SET rate_block_1=?, rate_blocks_2=?, rate_blocks_6=?, rate_blocks_12=?, rate_blocks_36=?, rate_blocks_72=?, rate_blocks_144=?, rate_blocks_1008=?, timestamp=?")) { update => - update.setLong(1, feeratesPerKB.block_1.toLong) - update.setLong(2, feeratesPerKB.blocks_2.toLong) - update.setLong(3, feeratesPerKB.blocks_6.toLong) - update.setLong(4, feeratesPerKB.blocks_12.toLong) - update.setLong(5, feeratesPerKB.blocks_36.toLong) - update.setLong(6, feeratesPerKB.blocks_72.toLong) - update.setLong(7, feeratesPerKB.blocks_144.toLong) - update.setLong(8, feeratesPerKB.blocks_1008.toLong) - update.setLong(9, TimestampMilli.now().toLong) - if (update.executeUpdate() == 0) { - using(sqlite.prepareStatement("INSERT INTO feerates_per_kb VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { insert => - insert.setLong(1, feeratesPerKB.block_1.toLong) - insert.setLong(2, feeratesPerKB.blocks_2.toLong) - insert.setLong(3, feeratesPerKB.blocks_6.toLong) - insert.setLong(4, feeratesPerKB.blocks_12.toLong) - insert.setLong(5, feeratesPerKB.blocks_36.toLong) - insert.setLong(6, feeratesPerKB.blocks_72.toLong) - insert.setLong(7, feeratesPerKB.blocks_144.toLong) - insert.setLong(8, feeratesPerKB.blocks_1008.toLong) - insert.setLong(9, TimestampMilli.now().toLong) - insert.executeUpdate() - } - } - } - } - - override def getFeerates(): Option[FeeratesPerKB] = { - using(sqlite.prepareStatement("SELECT rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_1008 FROM feerates_per_kb")) { statement => - statement.executeQuery() - .map { rs => - FeeratesPerKB( - // NB: we don't bother storing this value in the DB, because it's unused on mobile. - mempoolMinFee = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_1008"))), - block_1 = FeeratePerKB(Satoshi(rs.getLong("rate_block_1"))), - blocks_2 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_2"))), - blocks_6 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_6"))), - blocks_12 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_12"))), - blocks_36 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_36"))), - blocks_72 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_72"))), - blocks_144 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_144"))), - blocks_1008 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_1008")))) - } - .headOption - } - } - - // used by mobile apps - override def close(): Unit = sqlite.close() -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index a376a4295e..b05e743ffb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -34,7 +35,7 @@ object SqliteNetworkDb { val DB_NAME = "network" } -class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { +class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { import SqliteNetworkDb._ import SqliteUtils.ExtendedResultSet._ @@ -124,10 +125,10 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { using(sqlite.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -142,18 +143,19 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { val batchSize = 100 using(sqlite.prepareStatement(s"DELETE FROM channels WHERE short_channel_id IN (${List.fill(batchSize)("?").mkString(",")})")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), inTransaction = true) { statement => shortChannelIds.foreach(shortChannelId => { statement.setLong(1, shortChannelId.toLong) @@ -163,7 +165,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement(s"DELETE FROM pruned WHERE short_channel_id=?")) { statement => statement.setLong(1, shortChannelId.toLong) statement.executeUpdate() @@ -176,7 +178,4 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { statement.executeQuery().nonEmpty } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 2dce58d6e1..603298b646 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -16,14 +16,14 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -33,7 +33,7 @@ import java.util.UUID import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} -class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { +class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { import SqlitePaymentsDb._ import SqliteUtils.ExtendedResultSet._ @@ -58,9 +58,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.executeUpdate("DROP table _sent_payments_old") statement.executeUpdate("ALTER TABLE received_payments RENAME TO _received_payments_old") - // We make payment request expiration not null in the received_payments table. + // We make invoice expiration not null in the received_payments table. // When it was previously set to NULL the default expiry should apply. - statement.executeUpdate(s"UPDATE _received_payments_old SET expire_at = created_at + ${PaymentRequest.DEFAULT_EXPIRY_SECONDS} WHERE expire_at IS NULL") + statement.executeUpdate(s"UPDATE _received_payments_old SET expire_at = created_at + ${Bolt11Invoice.DEFAULT_EXPIRY_SECONDS} WHERE expire_at IS NULL") statement.executeUpdate("CREATE TABLE received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)") statement.executeUpdate("INSERT INTO received_payments (payment_hash, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) SELECT payment_hash, preimage, payment_request, received_msat, created_at, expire_at, received_at FROM _received_payments_old") statement.executeUpdate("DROP table _received_payments_old") @@ -132,7 +132,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setLong(7, sent.recipientAmount.toLong) statement.setBytes(8, sent.recipientNodeId.value.toArray) statement.setLong(9, sent.createdAt.toLong) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) statement.executeUpdate() } } @@ -178,7 +178,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVector("recipient_node_id")), TimestampMilli(rs.getLong("created_at")), - rs.getStringNullable("payment_request").map(PaymentRequest.read), + rs.getStringNullable("payment_request").map(Invoice.fromString(_).get), status ) } @@ -231,14 +231,14 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { + override def addIncomingPayment(invoice: Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setBytes(1, pr.paymentHash.toArray) + statement.setBytes(1, invoice.paymentHash.toArray) statement.setBytes(2, preimage.toArray) statement.setString(3, paymentType) - statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toLong.seconds.toMillis) + statement.setString(4, invoice.toString) + statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) + statement.setLong(6, (invoice.createdAt + invoice.relativeExpiry).toLong.seconds.toMillis) statement.executeUpdate() } } @@ -254,19 +254,19 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val paymentRequest = rs.getString("payment_request") + val invoice = Invoice.fromString(rs.getString("payment_request")).get IncomingPayment( - PaymentRequest.read(paymentRequest), + invoice, rs.getByteVector32("payment_preimage"), rs.getString("payment_type"), TimestampMilli(rs.getLong("created_at")), - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getLongNullable("received_at").map(TimestampMilli(_)))) + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getLongNullable("received_at").map(TimestampMilli(_)))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], invoice_opt: Option[Invoice], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired + case None if invoice_opt.exists(_.isExpired()) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -378,28 +378,24 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { val externalId_opt = rs.getStringNullable("external_id") val paymentHash = rs.getByteVector32("payment_hash") val paymentType = rs.getString("payment_type") - val paymentRequest_opt = rs.getStringNullable("payment_request") + val invoice_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") val createdAt = TimestampMilli(rs.getLong("created_at")) val completedAt_opt = rs.getLongNullable("completed_at").map(TimestampMilli(_)) val expireAt_opt = rs.getLongNullable("expire_at").map(TimestampMilli(_)) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) - PlainIncomingPayment(paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt.map(Invoice.fromString(_).get), completedAt_opt) + PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") // note that the resulting status will not contain any details (routes, failures...) val status: OutgoingPaymentStatus = buildOutgoingPaymentStatus(preimage_opt, None, None, completedAt_opt, None) - PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt) + PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt) } }.toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() - } object SqlitePaymentsDb { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 7b4e0f2f2c..610bb07909 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends @@ -35,7 +35,7 @@ object SqlitePeersDb { val CURRENT_VERSION = 2 } -class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { +class SqlitePeersDb(val sqlite: Connection) extends PeersDb with Logging { import SqlitePeersDb._ import SqliteUtils.ExtendedResultSet._ @@ -128,8 +128,4 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { ) } } - - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala index 82ae1111fa..351a229e74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.HtlcSettlementCommand import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends @@ -31,7 +31,7 @@ object SqlitePendingCommandsDb { val CURRENT_VERSION = 2 } -class SqlitePendingCommandsDb(sqlite: Connection) extends PendingCommandsDb with Logging { +class SqlitePendingCommandsDb(val sqlite: Connection) extends PendingCommandsDb with Logging { import SqlitePendingCommandsDb._ import SqliteUtils.ExtendedResultSet._ @@ -88,7 +88,4 @@ class SqlitePendingCommandsDb(sqlite: Connection) extends PendingCommandsDb with .toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 6fdc36afdb..3219d6f76f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -16,26 +16,26 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress - import akka.actor.{Props, _} import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected, Socks5Error} import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams} +import fr.acinq.eclair.wire.protocol._ +import java.net.InetSocketAddress import scala.concurrent.duration._ /** * Created by PM on 27/10/2015. * */ -class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean) extends Actor with DiagnosticActorLogging { +class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, remoteNodeAddress: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean) extends Actor with DiagnosticActorLogging { import context.system @@ -44,7 +44,14 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], def receive: Receive = { case Symbol("connect") => - val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteAddress, proxyParams))) match { + // note that there is no resolution here, it's either plain ip addresses, or unresolved tor hostnames + val remoteAddress = remoteNodeAddress match { + case addr: IPv4 => new InetSocketAddress(addr.ipv4, addr.port) + case addr: IPv6 => new InetSocketAddress(addr.ipv6, addr.port) + case addr: Tor2 => InetSocketAddress.createUnresolved(addr.host, addr.port) + case addr: Tor3 => InetSocketAddress.createUnresolved(addr.host, addr.port) + } + val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteNodeAddress, proxyParams))) match { case Some((proxyParams, Some(proxyAddress))) => log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") (proxyAddress, Some(proxyParams)) @@ -53,14 +60,14 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], (remoteAddress, None) } IO(Tcp) ! Tcp.Connect(peerOrProxyAddress, timeout = Some(20 seconds), options = KeepAlive(true) :: Nil, pullMode = true) - context become connecting(proxyParams_opt) + context become connecting(proxyParams_opt, remoteAddress) } - def connecting(proxyParams: Option[Socks5ProxyParams]): Receive = { + def connecting(proxyParams: Option[Socks5ProxyParams], remoteAddress: InetSocketAddress): Receive = { case Tcp.CommandFailed(c: Tcp.Connect) => val peerOrProxyAddress = c.remoteAddress log.info(s"connection failed to ${str(peerOrProxyAddress)}") - origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteNodeAddress)) context stop self case Tcp.Connected(peerOrProxyAddress, _) => @@ -75,7 +82,7 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], context become { case Tcp.CommandFailed(_: Socks5Connect) => log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") - origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteNodeAddress)) context stop self case Socks5Connected(_) => log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") @@ -127,13 +134,13 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], switchboard = switchboard, router = router )) - peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt, isPersistent = isPersistent) + peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteNodeAddress, origin_opt = origin_opt, isPersistent = isPersistent) peerConnection } } object Client { - def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean): Props = Props(new Client(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router, address, remoteNodeId, origin_opt, isPersistent)) + def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, address: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean): Props = Props(new Client(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router, address, remoteNodeId, origin_opt, isPersistent)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala index efcb6601d5..677cc6ca6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala @@ -16,16 +16,15 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress - import akka.actor.{Actor, ActorLogging, ActorRef, DeadLetter, Props} import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub import akka.cluster.pubsub.DistributedPubSubMediator.Put -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.tor.Socks5ProxyParams +import fr.acinq.eclair.wire.protocol.NodeAddress class ClientSpawner(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends Actor with ActorLogging { @@ -57,7 +56,7 @@ class ClientSpawner(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyP log.warning("handling outgoing connection request locally") self forward req case _: DeadLetter => - // we don't care about other dead letters + // we don't care about other dead letters } } @@ -65,8 +64,8 @@ object ClientSpawner { def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef): Props = Props(new ClientSpawner(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router)) - case class ConnectionRequest(address: InetSocketAddress, - remoteNodeId: PublicKey, + case class ConnectionRequest(remoteNodeId: PublicKey, + address: NodeAddress, origin: ActorRef, isPersistent: Boolean) extends RemoteTypes } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala index d20ba0cdc2..188f090396 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala @@ -20,8 +20,8 @@ import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.{ActorRef, typed} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.io.Peer.{PeerInfo, PeerInfoResponse} import fr.acinq.eclair.io.Switchboard.GetPeerInfo import fr.acinq.eclair.wire.protocol.OnionMessage diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala index 6e7736fd28..cb332da292 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair.io import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.wire.protocol.NodeAddress import scodec.bits.ByteVector import scala.util.{Failure, Success, Try} -case class NodeURI(nodeId: PublicKey, address: HostAndPort) { +case class NodeURI(nodeId: PublicKey, address: NodeAddress) { override def toString: String = s"$nodeId@$address" } @@ -40,10 +41,10 @@ object NodeURI { @throws[IllegalArgumentException] def parse(uri: String): NodeURI = { uri.split("@") match { - case Array(nodeId, address) => (Try(PublicKey(ByteVector.fromValidHex(nodeId))), Try(HostAndPort.fromString(address).withDefaultPort(DEFAULT_PORT))) match { - case (Success(pk), Success(hostAndPort)) => NodeURI(pk, hostAndPort) - case (Failure(_), _) => throw new IllegalArgumentException("Invalid node id") - case (_, Failure(_)) => throw new IllegalArgumentException("Invalid host:port") + case Array(nodeId, address) => (Try(PublicKey(ByteVector.fromValidHex(nodeId))), Try(HostAndPort.fromString(address)).flatMap(hostAndPort => NodeAddress.fromParts(hostAndPort.getHost, hostAndPort.getPortOrDefault(DEFAULT_PORT)))) match { + case (Success(pk), Success(nodeAddress)) => NodeURI(pk, nodeAddress) + case (Failure(t), _) => throw new IllegalArgumentException("Invalid node id", t) + case (_, Failure(t)) => throw new IllegalArgumentException("Invalid host:port", t) } case _ => throw new IllegalArgumentException("Invalid uri, should be nodeId@host:port") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 55da17df76..4ad26188b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -21,9 +21,8 @@ import akka.actor.{Actor, ActorContext, ActorRef, ExtendedActorSystem, FSM, OneF import akka.event.Logging.MDC import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout -import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator @@ -32,6 +31,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{OnChainAddressGenerator, OnChainChannelFunder} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.MessageRelay.Status import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.io.PeerConnection.KillReason @@ -42,8 +42,8 @@ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning} import scodec.bits.ByteVector -import java.net.InetSocketAddress -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} /** * This actor represents a logical peer. There is one [[Peer]] per unique remote node id at all time. @@ -99,6 +99,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId") stay() using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) + case Event(e: SpawnChannelInitiator, _) => + e.origin ! Status.Failure(new RuntimeException("channel creation failed: disconnected")) + stay() + + case Event(_: SpawnChannelNonInitiator, _) => stay() // we got disconnected before creating the channel actor + case Event(_: LightningMessage, _) => stay() // we probably just got disconnected and that's the last messages we received } @@ -141,46 +147,58 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.remoteFeatures.hasFeature(Wumbo)) { sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo")) stay() - } else if (c.fundingSatoshis > nodeParams.maxFundingSatoshis) { + } else if (c.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) { sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay() } else { - val channelConfig = ChannelConfig.standard // If a channel type was provided, we directly use it instead of computing it based on local and remote features. - val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = true, c.fundingSatoshis, origin_opt = Some(sender())) - c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) - val temporaryChannelId = randomBytes32() - val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) - val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) - log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelConfig, channelType) - stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) + val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = channelFlags.announceChannel)) + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + val origin = sender() + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis).andThen { + case Success(localParams) => selfRef ! SpawnChannelInitiator(c, ChannelConfig.standard, channelType, localParams, origin) + case Failure(t) => origin ! Status.Failure(new RuntimeException("channel creation failed", t)) + } + stay() } + case Event(SpawnChannelInitiator(c, channelConfig, channelType, localParams, origin), d: ConnectedData) => + val channel = spawnChannel(Some(origin)) + c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) + val temporaryChannelId = randomBytes32() + val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) + val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) + stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + case Event(msg: protocol.OpenChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val channelConfig = ChannelConfig.standard val chosenChannelType: Either[ChannelException, SupportedChannelType] = msg.channelType_opt match { // remote explicitly specifies a channel type: we check whether we want to allow it case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType)) + case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel), remoteChannelType)) } // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel)) } chosenChannelType match { case Right(channelType) => - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) - val temporaryChannelId = msg.temporaryChannelId - log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) - channel ! msg - stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, msg.fundingSatoshis).andThen { + case Success(localParams) => selfRef ! SpawnChannelNonInitiator(msg, ChannelConfig.standard, channelType, localParams) + case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(msg.temporaryChannelId, "channel creation failed"), d.peerConnection) + } + stay() case Left(ex) => log.warning(s"ignoring open_channel: ${ex.getMessage}") val err = Error(msg.temporaryChannelId, ex.getMessage) @@ -192,6 +210,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() } + case Event(SpawnChannelNonInitiator(open, channelConfig, channelType, localParams), d: ConnectedData) => + val channel = spawnChannel(None) + val temporaryChannelId = open.temporaryChannelId + log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! open + stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + case Event(msg: HasChannelId, d: ConnectedData) => d.channels.get(FinalChannelId(msg.channelId)) match { case Some(channel) => channel forward msg @@ -331,12 +357,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA def gotoConnected(connectionReady: PeerConnection.ConnectionReady, channels: Map[ChannelId, ActorRef]): State = { require(remoteNodeId == connectionReady.remoteNodeId, s"invalid nodeid: $remoteNodeId != ${connectionReady.remoteNodeId}") - log.debug("got authenticated connection to address {}:{}", connectionReady.address.getHostString, connectionReady.address.getPort) + log.debug("got authenticated connection to address {}", connectionReady.address) if (connectionReady.outgoing) { // we store the node address upon successful outgoing connection, so we can reconnect later // any previous address is overwritten - NodeAddress.fromParts(connectionReady.address.getHostString, connectionReady.address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress)) + nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, connectionReady.address) } // let's bring existing/requested channels online @@ -357,16 +383,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA s(e) } - def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { - val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) { - val walletKey = Helpers.getWalletPaymentBasepoint(wallet)(ExecutionContext.Implicits.global) - (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) + def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, isInitiator: Boolean, fundingAmount: Satoshi)(implicit ec: ExecutionContext): Future[LocalParams] = { + if (channelType.paysDirectlyToWallet) { + wallet.getReceivePubkey().map(walletKey => makeChannelParams(nodeParams, initFeatures, Script.write(Script.pay2wpkh(walletKey)), Some(walletKey), isInitiator, fundingAmount)) } else { - (Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash)(ExecutionContext.Implicits.global), None) + wallet.getReceiveAddress().map(address => makeChannelParams(nodeParams, initFeatures, Script.write(addressToPublicKeyScript(address, nodeParams.chainHash)), None, isInitiator, fundingAmount)) } - val localParams = makeChannelParams(nodeParams, initFeatures, finalScript, walletStaticPaymentBasepoint, funder, fundingAmount) - val channel = spawnChannel(origin_opt) - (channel, localParams) } def spawnChannel(origin_opt: Option[ActorRef]): ActorRef = { @@ -411,7 +433,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } override def mdc(currentMessage: Any): MDC = { - Logs.mdc(LogCategory(currentMessage), Some(remoteNodeId), Logs.channelId(currentMessage)) + Logs.mdc(LogCategory(currentMessage), Some(remoteNodeId), Logs.channelId(currentMessage), nodeAlias_opt = Some(nodeParams.alias)) } } @@ -445,10 +467,10 @@ object Peer { } case object Nothing extends Data { override def channels = Map.empty } case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data - case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { + case class ConnectedData(address: NodeAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit) - def localFeatures: Features = localInit.features - def remoteFeatures: Features = remoteInit.features + def localFeatures: Features[InitFeature] = localInit.features + def remoteFeatures: Features[InitFeature] = remoteInit.features } sealed trait State @@ -456,8 +478,8 @@ object Peer { case object DISCONNECTED extends State case object CONNECTED extends State - case class Init(storedChannels: Set[HasCommitments]) - case class Connect(nodeId: PublicKey, address_opt: Option[HostAndPort], replyTo: ActorRef, isPersistent: Boolean) { + case class Init(storedChannels: Set[PersistentChannelData]) + case class Connect(nodeId: PublicKey, address_opt: Option[NodeAddress], replyTo: ActorRef, isPersistent: Boolean) { def uri: Option[NodeURI] = address_opt.map(NodeURI(nodeId, _)) } object Connect { @@ -465,18 +487,22 @@ object Peer { } case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful - case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { - require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") - require(pushMsat >= 0.msat, s"pushMsat must be positive") + case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { + require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") + require(pushMsat <= fundingSatoshis, "pushMsat must be less or equal to fundingSatoshis") + require(fundingSatoshis >= 0.sat, "fundingSatoshis must be positive") + require(pushMsat >= 0.msat, "pushMsat must be positive") fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) } + private case class SpawnChannelInitiator(cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams, origin: ActorRef) + private case class SpawnChannelNonInitiator(msg: protocol.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams) + case class GetPeerInfo(replyTo: Option[typed.ActorRef[PeerInfoResponse]]) sealed trait PeerInfoResponse { def nodeId: PublicKey } - case class PeerInfo(peer: ActorRef, nodeId: PublicKey, state: State, address: Option[InetSocketAddress], channels: Int) extends PeerInfoResponse + case class PeerInfo(peer: ActorRef, nodeId: PublicKey, state: State, address: Option[NodeAddress], channels: Int) extends PeerInfoResponse case class PeerNotFound(nodeId: PublicKey) extends PeerInfoResponse { override def toString: String = s"peer $nodeId not found" } case class PeerRoutingMessage(peerConnection: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) extends RemoteTypes @@ -501,17 +527,17 @@ object Peer { case class RelayOnionMessage(messageId: ByteVector32, msg: OnionMessage, replyTo_opt: Option[typed.ActorRef[Status]]) // @formatter:on - def makeChannelParams(nodeParams: NodeParams, initFeatures: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isInitiator: Boolean, fundingAmount: Satoshi): LocalParams = { LocalParams( nodeParams.nodeId, - nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently - dustLimit = nodeParams.dustLimit, - maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, - channelReserve = (fundingAmount * nodeParams.reserveToFundingRatio).max(nodeParams.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimum = nodeParams.htlcMinimum, - toSelfDelay = nodeParams.toRemoteDelay, // we choose their delay - maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, - isFunder = isFunder, + nodeParams.channelKeyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key paths end differently + dustLimit = nodeParams.channelConf.dustLimit, + maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit + htlcMinimum = nodeParams.channelConf.htlcMinimum, + toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay + maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, + isInitiator = isInitiator, defaultFinalScriptPubKey = defaultFinalScriptPubkey, walletStaticPaymentBasepoint = walletStaticPaymentBasepoint, initFeatures = initFeatures) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index e470842c72..6297084eb5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -16,10 +16,10 @@ package fr.acinq.eclair.io -import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated} +import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.TransportHandler @@ -28,11 +28,10 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector -import java.net.InetSocketAddress import scala.concurrent.duration._ import scala.util.Random @@ -57,7 +56,7 @@ import scala.util.Random * * Created by PM on 11/03/2020. */ -class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends FSMDiagnosticActorLogging[PeerConnection.State, PeerConnection.Data] { +class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends FSMDiagnosticActorLogging[PeerConnection.State, PeerConnection.Data] with Stash { import PeerConnection._ @@ -87,8 +86,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A when(AUTHENTICATING) { case Event(TransportHandler.HandshakeCompleted(remoteNodeId), d: AuthenticatingData) => cancelTimer(AUTH_TIMER) - import d.pendingAuth.address - log.info(s"connection authenticated with $remoteNodeId@${address.getHostString}:${address.getPort} direction=${if (d.pendingAuth.outgoing) "outgoing" else "incoming"}") + log.info(s"connection authenticated (direction=${if (d.pendingAuth.outgoing) "outgoing" else "incoming"})") Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Authenticated).increment() switchboard ! Authenticated(self, remoteNodeId) goto(BEFORE_INIT) using BeforeInitData(remoteNodeId, d.pendingAuth, d.transport, d.isPersistent) @@ -97,6 +95,11 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A log.warning(s"authentication timed out after ${conf.authTimeout}") d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("authentication timed out")) stop(FSM.Normal) + + case Event(_: protocol.Init, _) => + log.debug("stashing remote init") + stash() + stay() } when(BEFORE_INIT) { @@ -104,13 +107,19 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.transport ! TransportHandler.Listener(self) Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment() log.info(s"using features=$localFeatures") - val localInit = IPAddress(d.pendingAuth.address.getAddress, d.pendingAuth.address.getPort) match { - case Some(remoteAddress) if !d.pendingAuth.outgoing && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) + val localInit = d.pendingAuth.address match { + case remoteAddress if !d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil))) } d.transport ! localInit startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout) + unstashAll() // unstash remote init if it already arrived goto(INITIALIZING) using InitializingData(chainHash, d.pendingAuth, d.remoteNodeId, d.transport, peer, localInit, doSync, d.isPersistent) + + case Event(_: protocol.Init, _) => + log.debug("stashing remote init") + stash() + stay() } when(INITIALIZING) { @@ -120,7 +129,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.transport ! TransportHandler.ReadAck(remoteInit) log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}") - remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our IP address is {} (public={})", address.socketAddress.toString, NodeAddress.isPublicIPAddress(address))) + remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our IP address is {} (public={})", address.toString, NodeAddress.isPublicIPAddress(address))) val featureGraphErr_opt = Features.validateFeatureGraph(remoteInit.features) if (remoteInit.networks.nonEmpty && remoteInit.networks.intersect(d.localInit.networks).isEmpty) { @@ -530,7 +539,8 @@ object PeerConnection { pingDisconnect: Boolean, maxRebroadcastDelay: FiniteDuration, killIdleDelay: FiniteDuration, - maxOnionMessagesPerSecond: Int) + maxOnionMessagesPerSecond: Int, + sendRemoteAddressInit: Boolean) // @formatter:off @@ -552,12 +562,12 @@ object PeerConnection { case object INITIALIZING extends State case object CONNECTED extends State - case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: InetSocketAddress, origin_opt: Option[ActorRef], transport_opt: Option[ActorRef] = None, isPersistent: Boolean) { + case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: NodeAddress, origin_opt: Option[ActorRef], transport_opt: Option[ActorRef] = None, isPersistent: Boolean) { def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance } case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey) extends RemoteTypes - case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features, doSync: Boolean) extends RemoteTypes - case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes + case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes + case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: NodeAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes sealed trait ConnectionResult extends RemoteTypes object ConnectionResult { @@ -569,7 +579,7 @@ object PeerConnection { } case object NoAddressFound extends ConnectionResult.Failure { override def toString: String = "no address found" } - case class ConnectionFailed(address: InetSocketAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" } + case class ConnectionFailed(address: NodeAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" } case class AuthenticationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } case class InitializationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } case class AlreadyConnected(peerConnection: ActorRef, peer: ActorRef) extends ConnectionResult.Failure with HasConnection { override def toString: String = "already connected" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala index de213c5c55..7c03d495d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala @@ -17,15 +17,13 @@ package fr.acinq.eclair.io import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.wire.protocol -import fr.acinq.eclair.wire.protocol.UnknownMessage - -import java.net.InetSocketAddress +import fr.acinq.eclair.wire.protocol.{NodeAddress, UnknownMessage} sealed trait PeerEvent -case class ConnectionInfo(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init) +case class ConnectionInfo(address: NodeAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init) case class PeerConnected(peer: ActorRef, nodeId: PublicKey, connectionInfo: ConnectionInfo) extends PeerEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index d976504504..a2c5602904 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -21,14 +21,12 @@ import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub import akka.cluster.pubsub.DistributedPubSubMediator.Send import akka.event.Logging.MDC -import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.wire.protocol.{NodeAddress, OnionAddress} import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, NodeParams, TimestampMilli} -import java.net.InetSocketAddress import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -130,12 +128,11 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends case Event(TickReconnect, _) => stay() - case Event(Peer.Connect(_, hostAndPort_opt, replyTo, isPersistent), _) => + case Event(Peer.Connect(_, address_opt, replyTo, isPersistent), _) => // manual connection requests happen completely independently of the automated reconnection process; // we initiate a connection but don't modify our state. // if we are already connecting/connected, the peer will kill any duplicate connections - hostAndPort_opt - .map(hostAndPort2InetSocketAddress) + address_opt .orElse(getPeerAddressFromDb(nodeParams, remoteNodeId)) match { case Some(address) => connect(address, origin = replyTo, isPersistent) case None => replyTo ! PeerConnection.ConnectionResult.NoAddressFound @@ -148,10 +145,10 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends // activate the extension only on demand, so that tests pass lazy val mediator = DistributedPubSub(context.system).mediator - private def connect(address: InetSocketAddress, origin: ActorRef, isPersistent: Boolean): Unit = { + private def connect(address: NodeAddress, origin: ActorRef, isPersistent: Boolean): Unit = { log.info(s"connecting to $address") - val req = ClientSpawner.ConnectionRequest(address, remoteNodeId, origin, isPersistent) - if (context.system.hasExtension(Cluster) && !address.getHostName.endsWith("onion")) { + val req = ClientSpawner.ConnectionRequest(remoteNodeId, address, origin, isPersistent) + if (context.system.hasExtension(Cluster)) { mediator ! Send(path = "/user/client-spawner", msg = req, localAffinity = false) } else { context.system.eventStream.publish(req) @@ -160,7 +157,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends } override def mdc(currentMessage: Any): MDC = { - Logs.mdc(Some(LogCategory.CONNECTION), Some(remoteNodeId)) + Logs.mdc(Some(LogCategory.CONNECTION), Some(remoteNodeId), nodeAlias_opt = Some(nodeParams.alias)) } } @@ -185,7 +182,7 @@ object ReconnectionTask { sealed trait Data case object Nothing extends Data case class IdleData(previousData: Data, since: TimestampMilli = TimestampMilli.now()) extends Data - case class ConnectingData(to: InetSocketAddress, nextReconnectionDelay: FiniteDuration) extends Data + case class ConnectingData(to: NodeAddress, nextReconnectionDelay: FiniteDuration) extends Data case class WaitingData(nextReconnectionDelay: FiniteDuration) extends Data // @formatter:on @@ -213,13 +210,11 @@ object ReconnectionTask { } } - def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[InetSocketAddress] = { + def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[NodeAddress] = { val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toSeq.flatMap(_.addresses) - selectNodeAddress(nodeParams, nodeAddresses).map(_.socketAddress) + selectNodeAddress(nodeParams, nodeAddresses) } - def hostAndPort2InetSocketAddress(hostAndPort: HostAndPort): InetSocketAddress = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort) - /** * This helps prevent peers reconnection loops due to synchronization of reconnection attempts. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala index b4d3542754..4dcda42af6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.io import java.net.InetSocketAddress - import akka.Done import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props} import akka.event.Logging.MDC @@ -26,6 +25,7 @@ import akka.io.{IO, Tcp} import fr.acinq.eclair.Logs import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair +import fr.acinq.eclair.wire.protocol.{IPAddress, NodeAddress} import scala.concurrent.Promise @@ -62,7 +62,7 @@ class Server(keyPair: KeyPair, peerConnectionConf: PeerConnection.Conf, switchbo switchboard = switchboard, router = router )) - peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = None, address = remote, origin_opt = None, isPersistent = true) + peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = None, address = IPAddress(remote.getAddress, remote.getPort), origin_opt = None, isPersistent = true) listener ! ResumeAccepting(batchSize = 1) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index d3f958db69..bfcd514462 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -19,9 +19,8 @@ package fr.acinq.eclair.io import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, typed} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.NodeParams +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ @@ -30,6 +29,7 @@ import fr.acinq.eclair.io.Peer.PeerInfoResponse import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router.RouterConf import fr.acinq.eclair.wire.protocol.OnionMessage +import fr.acinq.eclair.{SubscriptionsComplete, NodeParams} /** * Ties network connections to peers. @@ -41,6 +41,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[LastChannelClosed]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) // we load channels from database private def initialPeersWithChannels(): Set[PublicKey] = { @@ -128,7 +129,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) def createPeer(remoteNodeId: PublicKey): ActorRef = peerFactory.spawn(context, remoteNodeId) - def createOrGetPeer(remoteNodeId: PublicKey, offlineChannels: Set[HasCommitments]): ActorRef = { + def createOrGetPeer(remoteNodeId: PublicKey, offlineChannels: Set[PersistentChannelData]): ActorRef = { getPeer(remoteNodeId) match { case Some(peer) => peer case None => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 48852b5824..fedb546410 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.json import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.balance.CheckBalance.{CorrectedOnChainBalance, GlobalBalance, OffChainBalance} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ @@ -30,12 +31,12 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.payment.PaymentFailure.PaymentFailedSummary import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.{ChannelHop, Route} +import fr.acinq.eclair.router.Router.{ChannelHop, ChannelRelayParams, Route} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature, channel} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -106,6 +107,10 @@ object ByteVector32KeySerializer extends MinimalKeySerializer({ case x: ByteVector32 => x.toHex }) +object ByteVector32KmpSerializer extends MinimalSerializer({ + case x: fr.acinq.bitcoin.ByteVector32 => JString(x.toHex) +}) + object ByteVector64Serializer extends MinimalSerializer({ case x: ByteVector64 => JString(x.toHex) }) @@ -146,6 +151,10 @@ object CltvExpiryDeltaSerializer extends MinimalSerializer({ case x: CltvExpiryDelta => JInt(x.toInt) }) +object BlockHeightSerializer extends MinimalSerializer({ + case x: BlockHeight => JInt(x.toInt) +}) + object FeeratePerKwSerializer extends MinimalSerializer({ case x: FeeratePerKw => JLong(x.toLong) }) @@ -206,6 +215,10 @@ object TransactionSerializer extends MinimalSerializer({ )) }) +object KeyPathSerializer extends MinimalSerializer({ + case x: KeyPath => JObject(JField("path", JArray(x.path.map(x => JLong(x)).toList))) +}) + object TransactionWithInputInfoSerializer extends MinimalSerializer({ case x: HtlcSuccessTx => JObject(List( JField("txid", JString(x.tx.txid.toHex)), @@ -281,8 +294,9 @@ object ColorSerializer extends MinimalSerializer({ }) // @formatter:off -private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHop]) -object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops)) +private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams) +private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHopJson]) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)))) private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { @@ -294,7 +308,7 @@ object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { }) private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId]) -object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.lastUpdate.shortChannelId))) +object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.shortChannelId))) // @formatter:on // @formatter:off @@ -331,7 +345,7 @@ object FailureTypeSerializer extends MinimalSerializer({ }) object NodeAddressSerializer extends MinimalSerializer({ - case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) + case n: NodeAddress => JString(n.toString) }) // @formatter:off @@ -339,12 +353,16 @@ private case class DirectedHtlcJson(direction: String, add: UpdateAddHtlc) object DirectedHtlcSerializer extends ConvertClassSerializer[DirectedHtlc](h => DirectedHtlcJson(direction = h.direction, add = h.add)) // @formatter:on -object PaymentRequestSerializer extends MinimalSerializer({ - case p: PaymentRequest => - val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq - val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq - val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq - val features = JField("features", Extraction.decompose(p.features.features)( +object InvoiceSerializer extends MinimalSerializer({ + case p: Bolt11Invoice => + val expiry = p.tags + .collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong } // NB: we look at fields directly because the value has a spec-defined default + .map(ex => JField("expiry", JLong(ex))).toSeq + val minFinalCltvExpiry = p.tags + .collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } // NB: we look at fields directly because the value has a spec-defined default + .map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq + val amount = p.amount_opt.map(msat => JField("amount", JLong(msat.toLong))).toSeq + val features = JField("features", Extraction.decompose(p.features)( DefaultFormats + FeatureKeySerializer + FeatureSupportSerializer + @@ -362,9 +380,9 @@ object PaymentRequestSerializer extends MinimalSerializer({ )) val fieldList = List( JField("prefix", JString(p.prefix)), - JField("timestamp", JLong(p.timestamp.toLong)), + JField("timestamp", JLong(p.createdAt.toLong)), JField("nodeId", JString(p.nodeId.toString())), - JField("serialized", JString(PaymentRequest.write(p))), + JField("serialized", JString(p.toString)), p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), JField("paymentHash", JString(p.paymentHash.toString()))) ++ paymentMetadata ++ @@ -375,6 +393,23 @@ object PaymentRequestSerializer extends MinimalSerializer({ routingInfo JObject(fieldList) + case invoice: Invoice => + val features = JField("features", Extraction.decompose(invoice.features)( + DefaultFormats + + FeatureKeySerializer + + FeatureSupportSerializer + + UnknownFeatureSerializer + )) + JObject( + JField("serialized", JString(invoice.toString)), + JField("amount", JLong(invoice.amount_opt.get.toLong)), + JField("nodeId", JString(invoice.nodeId.toString())), + invoice.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), + JField("paymentHash", JString(invoice.paymentHash.toString())), + JField("timestamp", JLong(invoice.createdAt.toLong)), + JField("relativeExpiry", JLong(invoice.relativeExpiry.toSeconds)), + JField("minFinalCltvExpiry", JInt(invoice.minFinalCltvExpiryDelta.toInt)), + features) }) object JavaUUIDSerializer extends MinimalSerializer({ @@ -385,10 +420,10 @@ object ChannelEventSerializer extends MinimalSerializer({ case e: ChannelCreated => JObject( JField("type", JString("channel-opened")), JField("remoteNodeId", JString(e.remoteNodeId.toString())), - JField("isFunder", JBool(e.isFunder)), + JField("isInitiator", JBool(e.isInitiator)), JField("temporaryChannelId", JString(e.temporaryChannelId.toHex)), - JField("initialFeeratePerKw", JLong(e.initialFeeratePerKw.toLong)), - JField("fundingTxFeeratePerKw", e.fundingTxFeeratePerKw.map(f => JLong(f.toLong)).getOrElse(JNothing)) + JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)), + JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing)) ) case e: ChannelStateChanged => JObject( JField("type", JString("channel-state-changed")), @@ -422,26 +457,31 @@ object OriginSerializer extends MinimalSerializer({ private case class GlobalBalanceJson(total: Btc, onChain: CorrectedOnChainBalance, offChain: OffChainBalance) object GlobalBalanceSerializer extends ConvertClassSerializer[GlobalBalance](b => GlobalBalanceJson(b.total, b.onChain, b.offChain)) -private case class PeerInfoJson(nodeId: PublicKey, state: String, address: Option[InetSocketAddress], channels: Int) -object PeerInfoSerializer extends ConvertClassSerializer[Peer.PeerInfo](peerInfo => PeerInfoJson(peerInfo.nodeId, peerInfo.state.toString, peerInfo.address, peerInfo.channels)) +private case class PeerInfoJson(nodeId: PublicKey, state: String, address: Option[String], channels: Int) +object PeerInfoSerializer extends ConvertClassSerializer[Peer.PeerInfo](peerInfo => PeerInfoJson(peerInfo.nodeId, peerInfo.state.toString, peerInfo.address.map(_.toString), peerInfo.channels)) private[json] case class MessageReceivedJson(pathId: Option[ByteVector], encodedReplyPath: Option[String], replyPath: Option[BlindedRoute], unknownTlvs: Map[String, ByteVector]) object OnionMessageReceivedSerializer extends ConvertClassSerializer[OnionMessages.ReceiveMessage](m => MessageReceivedJson(m.pathId, m.finalPayload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex), m.finalPayload.replyPath.map(_.blindedRoute), m.finalPayload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap)) // @formatter:on -case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { +// @formatter:off +/** this is cosmetic, just to not have a '_opt' field in json, which will only appear if the option is defined anyway */ +private case class ShortIdsJson(real: RealScidStatus, localAlias: Alias, remoteAlias: Option[ShortChannelId]) +object ShortIdsSerializer extends ConvertClassSerializer[ShortIds](s => ShortIdsJson(s.real, s.localAlias, s.remoteAlias_opt)) +// @formatter:on + +case class CustomTypeHints(custom: Map[Class[_], String], override val typeHintFieldName: String = "type") extends TypeHints { val reverse: Map[String, Class[_]] = custom.map(_.swap) override val hints: List[Class[_]] = custom.keys.toList - override def hintFor(clazz: Class[_]): String = custom.getOrElse(clazz, { - throw new IllegalArgumentException(s"No type hint mapping found for $clazz") - }) + override def hintFor(clazz: Class[_]): Option[String] = custom.get(clazz) - override def classFor(hint: String): Option[Class[_]] = reverse.get(hint) + override def classFor(hint: String, parent: Class[_]): Option[Class[_]] = reverse.get(hint) } object CustomTypeHints { + val incomingPaymentStatus: CustomTypeHints = CustomTypeHints(Map( IncomingPaymentStatus.Pending.getClass -> "pending", IncomingPaymentStatus.Expired.getClass -> "expired", @@ -467,6 +507,11 @@ object CustomTypeHints { classOf[MessageReceivedJson] -> "onion-message-received" )) + val channelSources: CustomTypeHints = CustomTypeHints(Map( + classOf[ChannelRelayParams.FromAnnouncement] -> "announcement", + classOf[ChannelRelayParams.FromHint] -> "hint" + )) + val channelStates: ShortTypeHints = ShortTypeHints( List( classOf[Nothing], @@ -475,26 +520,34 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], - classOf[DATA_WAIT_FOR_FUNDING_LOCKED], + classOf[DATA_WAIT_FOR_CHANNEL_READY], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], classOf[DATA_NORMAL], classOf[DATA_SHUTDOWN], classOf[DATA_NEGOTIATING], classOf[DATA_CLOSING], classOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - )) + ), typeHintFieldName = "type") + + val realScidStatuses: CustomTypeHints = CustomTypeHints(Map( + classOf[RealScidStatus.Unknown.type] -> "unknown", + classOf[RealScidStatus.Temporary] -> "temporary", + classOf[RealScidStatus.Final] -> "final", + ), typeHintFieldName = "status") } object JsonSerializers { implicit val serialization: Serialization.type = jackson.Serialization - implicit val formats: Formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + + implicit val formats: Formats = org.json4s.DefaultFormats + CustomTypeHints.incomingPaymentStatus + CustomTypeHints.outgoingPaymentStatus + CustomTypeHints.paymentEvent + CustomTypeHints.onionMessageEvent + + CustomTypeHints.channelSources + CustomTypeHints.channelStates + + CustomTypeHints.realScidStatuses + ByteVectorSerializer + ByteVector32Serializer + ByteVector64Serializer + @@ -507,6 +560,7 @@ object JsonSerializers { MilliSatoshiSerializer + CltvExpirySerializer + CltvExpiryDeltaSerializer + + BlockHeightSerializer + FeeratePerKwSerializer + ShortChannelIdSerializer + ChannelIdentifierSerializer + @@ -516,6 +570,7 @@ object JsonSerializers { PrivateKeySerializer + TransactionSerializer + TransactionWithInputInfoSerializer + + KeyPathSerializer + InetSocketAddressSerializer + OutPointSerializer + OutPointKeySerializer + @@ -533,13 +588,14 @@ object JsonSerializers { FailureTypeSerializer + NodeAddressSerializer + DirectedHtlcSerializer + - PaymentRequestSerializer + + InvoiceSerializer + JavaUUIDSerializer + OriginSerializer + ByteVector32KeySerializer + GlobalBalanceSerializer + PeerInfoSerializer + PaymentFailedSummarySerializer + - OnionMessageReceivedSerializer + OnionMessageReceivedSerializer + + ShortIdsSerializer } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala index 5af9696ea8..20f525a188 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -16,7 +16,8 @@ package fr.acinq.eclair.message -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.MessageRelay.RelayPolicy import fr.acinq.eclair.wire.protocol.MessageOnion.{BlindedFinalPayload, BlindedRelayPayload, FinalPayload, RelayPayload} @@ -32,18 +33,22 @@ import scala.util.{Failure, Success} object OnionMessages { - case class OnionMessageConfig(relayPolicy: RelayPolicy, timeout: FiniteDuration) + case class OnionMessageConfig(relayPolicy: RelayPolicy, + timeout: FiniteDuration, + maxAttempts: Int) case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None) - case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) + sealed trait Destination + case class BlindedPath(route: Sphinx.RouteBlinding.BlindedRoute) extends Destination + case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) extends Destination def buildRoute(blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], - destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute]): Sphinx.RouteBlinding.BlindedRoute = { + destination: Destination): Sphinx.RouteBlinding.BlindedRoute = { val last = destination match { - case Left(Recipient(nodeId, _, _)) => OutgoingNodeId(nodeId) :: Nil - case Right(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OutgoingNodeId(nodeId) :: NextBlinding(blindingKey) :: Nil + case Recipient(nodeId, _, _) => OutgoingNodeId(nodeId) :: Nil + case BlindedPath(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OutgoingNodeId(nodeId) :: NextBlinding(blindingKey) :: Nil } val intermediatePayloads = if (intermediateNodes.isEmpty) { @@ -55,11 +60,11 @@ object OnionMessages { .map(MessageOnionCodecs.blindedRelayPayloadCodec.encode(_).require.bytes) } destination match { - case Left(Recipient(nodeId, pathId, padding)) => + case Recipient(nodeId, pathId, padding) => val tlvs = padding.map(Padding(_) :: Nil).getOrElse(Nil) ++ pathId.map(PathId(_) :: Nil).getOrElse(Nil) val lastPayload = MessageOnionCodecs.blindedFinalPayloadCodec.encode(BlindedFinalPayload(TlvStream(tlvs))).require.bytes Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload) - case Right(route) => + case BlindedPath(route) => if (intermediateNodes.isEmpty) { route } else { @@ -82,11 +87,11 @@ object OnionMessages { def buildMessage(sessionKey: PrivateKey, blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], - destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute], + destination: Destination, content: Seq[OnionMessagePayloadTlv], - userCustomTlvs: Seq[GenericTlv] = Nil): (PublicKey, OnionMessage) = { + userCustomTlvs: TlvStream[OnionMessagePayloadTlv] = TlvStream.empty): (PublicKey, OnionMessage) = { val route = buildRoute(blindingSecret, intermediateNodes, destination) - val lastPayload = MessageOnionCodecs.finalPerHopPayloadCodec.encode(FinalPayload(TlvStream(EncryptedData(route.encryptedPayloads.last) +: content, userCustomTlvs))).require.bytes + val lastPayload = MessageOnionCodecs.finalPerHopPayloadCodec.encode(FinalPayload(TlvStream[OnionMessagePayloadTlv](EncryptedData(route.encryptedPayloads.last) +: (content ++ userCustomTlvs.records), userCustomTlvs.unknown))).require.bytes val payloads = route.encryptedPayloads.dropRight(1).map(encTlv => MessageOnionCodecs.relayPerHopPayloadCodec.encode(RelayPayload(TlvStream(EncryptedData(encTlv)))).require.bytes) :+ lastPayload val payloadSize = payloads.map(_.length + Sphinx.MacLength).sum val packetSize = if (payloadSize <= 1300) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index 171c09f47a..0a04506421 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.message import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.io.{MessageRelay, Switchboard} import fr.acinq.eclair.message.OnionMessages.ReceiveMessage import fr.acinq.eclair.randomBytes32 @@ -43,7 +43,7 @@ object Postman { private case class WrappedMessage(finalPayload: FinalPayload, pathId: Option[ByteVector]) extends Command sealed trait OnionMessageResponse case object NoReply extends OnionMessageResponse - case class Response(payload: FinalPayload) extends OnionMessageResponse + case class Response(payload: FinalPayload, pathId: ByteVector32) extends OnionMessageResponse case class SendingStatus(status: MessageRelay.Status) extends OnionMessageResponse with Command // @formatter:on @@ -53,58 +53,72 @@ object Postman { val relayMessageStatusAdapter = context.messageAdapter[MessageRelay.Status](SendingStatus) + // For messages expecting a reply, send reply or failure to send val subscribed = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() + + // For messages not expecting a reply, send success or failure to send val sendStatusTo = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() - val sendFailureTo = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() - Behaviors.receiveMessagePartial { + // For pairs or messages expecting one reply, send first reply or second failure + val messagePair = new mutable.HashMap[ByteVector32, (ByteVector32, ActorRef[OnionMessageResponse])]() + + Behaviors.receiveMessage { case WrappedMessage(finalPayload, Some(pathId)) if pathId.length == 32 => - subscribed.get(ByteVector32(pathId)) match { - case Some(ref) => - subscribed -= ByteVector32(pathId) - ref ! Response(finalPayload) - case None => () // ignoring message with unknown pathId + val id = ByteVector32(pathId) + subscribed.get(id).foreach(ref => { + subscribed -= id + ref ! Response(finalPayload, id) + }) + messagePair.get(id).foreach { case (otherId, ref) => + messagePair -= id + messagePair -= otherId + ref ! Response(finalPayload, id) } Behaviors.same case WrappedMessage(_, _) => // ignoring message with invalid or missing pathId Behaviors.same - case SendMessage(nextNodeId, message, None, ref, _) => + case SendMessage(nextNodeId, message, None, ref, _) => // not expecting reply val messageId = randomBytes32() sendStatusTo += (messageId -> ref) switchboard ! Switchboard.RelayMessage(messageId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same - case SendMessage(nextNodeId, message, Some(pathId), ref, timeout) => - val messageId = randomBytes32() - sendFailureTo += (messageId -> ref) + case SendMessage(nextNodeId, message, Some(pathId), ref, timeout) => // expecting reply subscribed += (pathId -> ref) context.scheduleOnce(timeout, context.self, Unsubscribe(pathId)) - switchboard ! Switchboard.RelayMessage(messageId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) + switchboard ! Switchboard.RelayMessage(pathId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same case Unsubscribe(pathId) => - subscribed.get(pathId).foreach(_ ! NoReply) - subscribed -= pathId + subscribed.get(pathId).foreach(ref => { + subscribed -= pathId + ref ! NoReply + }) + messagePair.get(pathId).foreach { case (otherId, ref) => + messagePair -= pathId + messagePair -= otherId + ref ! NoReply + } Behaviors.same case status@SendingStatus(MessageRelay.Sent(messageId)) => - sendStatusTo.get(messageId) match { - case Some(ref) => - sendStatusTo -= messageId - ref ! status - case None => () - } + sendStatusTo.get(messageId).foreach(ref => { + sendStatusTo -= messageId + ref ! status + }) Behaviors.same case SendingStatus(status: MessageRelay.Failure) => - sendStatusTo.get(status.messageId) match { - case Some(ref) => - sendStatusTo -= status.messageId - ref ! SendingStatus(status) - case None => () - } - sendFailureTo.get(status.messageId) match { - case Some(ref) => - sendFailureTo -= status.messageId - ref ! SendingStatus(status) - case None => () + sendStatusTo.get(status.messageId).foreach(ref => { + sendStatusTo -= status.messageId + ref ! SendingStatus(status) + }) + subscribed.get(status.messageId).foreach(ref => { + subscribed -= status.messageId + ref ! SendingStatus(status) + }) + messagePair.get(status.messageId).foreach { case (otherId, ref) => + messagePair -= status.messageId + if (!messagePair.contains(otherId)) { + ref ! NoReply + } } Behaviors.same } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index a60205ce68..8f5852e22b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -16,11 +16,11 @@ package fr.acinq -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.eclair.crypto.StrongRandom import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.wire.protocol.ChannelUpdate import scodec.Attempt import scodec.bits.{BitVector, ByteVector} @@ -74,22 +74,31 @@ package object eclair { def nodeFee(relayFees: RelayFees, paymentAmount: MilliSatoshi): MilliSatoshi = nodeFee(relayFees.feeBase, relayFees.feeProportionalMillionths, paymentAmount) - def nodeFee(channelUpdate: ChannelUpdate, paymentAmount: MilliSatoshi): MilliSatoshi = nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, paymentAmount) - /** * @param address base58 of bech32 address * @param chainHash hash of the chain we're on, which will be checked against the input address * @return the public key script that matches the input address. */ def addressToPublicKeyScript(address: String, chainHash: ByteVector32): Seq[ScriptElt] = { - Try(Base58Check.decode(address)) match { + + def decodeBase58(input: String): (Byte, ByteVector) = { + val decoded = Base58Check.decode(input) + (decoded.getFirst.byteValue(), ByteVector.view(decoded.getSecond)) + } + + def decodeBech32(input: String): (String, Byte, ByteVector) = { + val decoded = Bech32.decodeWitnessAddress(input) + (decoded.getFirst, decoded.getSecond.byteValue(), ByteVector.view(decoded.getThird)) + } + + Try(decodeBase58(address)) match { case Success((Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)) if chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash => Script.pay2pkh(pubKeyHash) case Success((Base58.Prefix.PubkeyAddress, pubKeyHash)) if chainHash == Block.LivenetGenesisBlock.hash => Script.pay2pkh(pubKeyHash) case Success((Base58.Prefix.ScriptAddressTestnet, scriptHash)) if chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil case Success((Base58.Prefix.ScriptAddress, scriptHash)) if chainHash == Block.LivenetGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil case Success(_) => throw new IllegalArgumentException("base58 address does not match our blockchain") case Failure(base58error) => - Try(Bech32.decodeWitnessAddress(address)) match { + Try(decodeBech32(address)) match { case Success((_, version, _)) if version != 0.toByte => throw new IllegalArgumentException(s"invalid version $version in bech32 address") case Success((_, _, bin)) if bin.length != 20 && bin.length != 32 => throw new IllegalArgumentException("hash length in bech32 address must be either 20 or 32 bytes") case Success(("bc", _, bin)) if chainHash == Block.LivenetGenesisBlock.hash => OP_0 :: OP_PUSHDATA(bin) :: Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala similarity index 68% rename from eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 6557e89433..03857806dc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -16,91 +16,89 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} -import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} /** - * Lightning Payment Request - * see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md + * Lightning Bolt 11 invoice + * see https://github.com/lightning/bolts/blob/master/11-payment-encoding.md * - * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet - * @param amount amount to pay (empty string means no amount is specified) - * @param timestamp request timestamp (UNIX format) - * @param nodeId id of the node emitting the payment request - * @param tags payment tags; must include a single PaymentHash tag and a single PaymentSecret tag. - * @param signature request signature that will be checked against node id + * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet + * @param amount_opt amount to pay (empty string means no amount is specified) + * @param createdAt invoice timestamp (UNIX format) + * @param nodeId id of the node emitting the invoice + * @param tags payment tags; must include a single PaymentHash tag and a single PaymentSecret tag. + * @param signature invoice signature that will be checked against node id */ -case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: TimestampSecond, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) { - - amount.foreach(a => require(a > 0.msat, s"amount is not valid")) - require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") - require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") - private val featuresErr = Features.validateFeatureGraph(features.features) - require(featuresErr.isEmpty, featuresErr.map(_.message)) - if (features.allowPaymentSecret) { - require(tags.collect { case _: PaymentRequest.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") +case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], createdAt: TimestampSecond, nodeId: PublicKey, tags: List[Bolt11Invoice.TaggedField], signature: ByteVector) extends Invoice { + + import fr.acinq.eclair.payment.Bolt11Invoice._ + + amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid")) + require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") + require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") + + { + val featuresErr = Features.validateFeatureGraph(features) + require(featuresErr.isEmpty, featuresErr.map(_.message)) + } + if (features.hasFeature(Features.PaymentSecret)) { + require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") } /** * @return the payment hash */ - lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHash => p.hash }.get + lazy val paymentHash = tags.collectFirst { case p: Bolt11Invoice.PaymentHash => p.hash }.get /** * @return the payment secret */ - lazy val paymentSecret = tags.collectFirst { case p: PaymentRequest.PaymentSecret => p.secret } + lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret } /** * @return the description of the payment, or its hash */ lazy val description: Either[String, ByteVector32] = tags.collectFirst { - case PaymentRequest.Description(d) => Left(d) - case PaymentRequest.DescriptionHash(h) => Right(h) + case Bolt11Invoice.Description(d) => Left(d) + case Bolt11Invoice.DescriptionHash(h) => Right(h) }.get /** * @return metadata about the payment (see option_payment_metadata). */ - lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: PaymentRequest.PaymentMetadata => m.data } + lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: Bolt11Invoice.PaymentMetadata => m.data } /** * @return the fallback address if any. It could be a script address, pubkey address, .. */ - def fallbackAddress(): Option[String] = tags.collectFirst { - case f: PaymentRequest.FallbackAddress => PaymentRequest.FallbackAddress.toAddress(f, prefix) - } + def fallbackAddress(): Option[String] = tags.collectFirst { case f: Bolt11Invoice.FallbackAddress => Bolt11Invoice.FallbackAddress.toAddress(f, prefix) } lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path } - lazy val expiry: Option[Long] = tags.collectFirst { - case expiry: PaymentRequest.Expiry => expiry.toLong - } + lazy val extraEdges: Seq[Invoice.ExtraEdge] = routingInfo.flatMap(path => toExtraEdges(path, nodeId)) - lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst { - case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta - } + lazy val relativeExpiry: FiniteDuration = FiniteDuration(tags.collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong }.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) - lazy val features: PaymentRequestFeatures = tags.collectFirst { case f: PaymentRequestFeatures => f }.getOrElse(PaymentRequestFeatures(BitVector.empty)) + lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = tags.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta }.getOrElse(DEFAULT_MIN_CLTV_EXPIRY_DELTA) - def isExpired: Boolean = expiry match { - case Some(expiryTime) => timestamp + expiryTime <= TimestampSecond.now() - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() - } + lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature]) /** - * @return the hash of this payment request + * @return the hash of this payment invoice */ def hash: ByteVector32 = { - val hrp = s"$prefix${Amount.encode(amount)}".getBytes("UTF-8") - val data = Bolt11Data(timestamp, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next + val hrp = s"$prefix${Amount.encode(amount_opt)}".getBytes("UTF-8") + val data = Bolt11Data(createdAt, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next val bin = Codecs.bolt11DataCodec.encode(data).require val message = ByteVector.view(hrp) ++ bin.dropRight(520).toByteVector Crypto.sha256(message) @@ -108,9 +106,9 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam /** * @param priv private key - * @return a signed payment request + * @return a signed payment invoice */ - def sign(priv: PrivateKey): PaymentRequest = { + def sign(priv: PrivateKey): Bolt11Invoice = { val sig64 = Crypto.sign(hash, priv) // in order to tell what the recovery id is, we actually recover the pubkey ourselves and compare it to the real one val pub0 = Crypto.recoverPublicKey(sig64, hash, 0.toByte) @@ -118,11 +116,23 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam val signature = sig64 :+ recid this.copy(signature = signature) } -} -object PaymentRequest { + /** + * @return a bech32-encoded payment invoice + */ + override def toString: String = { + // currency unit is Satoshi, but we compute amounts in Millisatoshis + val hramount = Amount.encode(amount_opt) + val hrp = s"${prefix}$hramount" + val data = Codecs.bolt11DataCodec.encode(Bolt11Data(createdAt, tags, signature)).require + val int5s = eight2fiveCodec.decode(data).require.value + Bech32.encode(hrp, int5s.toArray, Bech32.Encoding.Bech32) + } +} +object Bolt11Invoice { val DEFAULT_EXPIRY_SECONDS: Long = 3600 + val DEFAULT_MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) val prefixes = Map( Block.RegtestGenesisBlock.hash -> "lnbcrt", @@ -130,10 +140,7 @@ object PaymentRequest { Block.LivenetGenesisBlock.hash -> "lnbc" ) - val defaultFeatures: Map[Feature with InvoiceFeature, FeatureSupport] = Map( - Features.VariableLengthOnion -> FeatureSupport.Mandatory, - Features.PaymentSecret -> FeatureSupport.Mandatory, - ) + val defaultFeatures: Features[InvoiceFeature] = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory)) def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], @@ -147,8 +154,8 @@ object PaymentRequest { timestamp: TimestampSecond = TimestampSecond.now(), paymentSecret: ByteVector32 = randomBytes32(), paymentMetadata: Option[ByteVector] = None, - features: PaymentRequestFeatures = PaymentRequestFeatures(defaultFeatures)): PaymentRequest = { - require(features.requirePaymentSecret, "invoices must require a payment secret") + features: Features[InvoiceFeature] = defaultFeatures): Bolt11Invoice = { + require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") val prefix = prefixes(chainHash) val tags = { val defaultTags = List( @@ -159,15 +166,16 @@ object PaymentRequest { fallbackAddress.map(FallbackAddress(_)), expirySeconds.map(Expiry(_)), Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), - Some(features) + // We want to keep invoices as small as possible, so we explicitly remove unknown features. + Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped())) ).flatten val routingInfoTags = extraHops.map(RoutingInfo) defaultTags ++ routingInfoTags } - PaymentRequest( + Bolt11Invoice( prefix = prefix, - amount = amount, - timestamp = timestamp, + amount_opt = amount, + createdAt = timestamp, nodeId = privateKey.publicKey, tags = tags, signature = ByteVector.empty @@ -227,15 +235,15 @@ object PaymentRequest { /** * Description * - * @param description a free-format string that will be included in the payment request + * @param description a free-format string that will be included in the invoice */ case class Description(description: String) extends TaggedField /** * Hash * - * @param hash hash that will be included in the payment request, and can be checked against the hash of a - * long description, an invoice, ... + * @param hash hash that will be included in the invoice, and can be checked against the hash of a + * long description, an SKU, ... */ case class DescriptionHash(hash: ByteVector32) extends TaggedField @@ -259,7 +267,10 @@ object PaymentRequest { } def fromBase58Address(address: String): FallbackAddress = { - val (prefix, hash) = Base58Check.decode(address) + val (prefix, hash) = { + val decoded = Base58Check.decode(address) + (decoded.getFirst.byteValue(), ByteVector.view(decoded.getSecond)) + } prefix match { case Base58.Prefix.PubkeyAddress => FallbackAddress(17.toByte, hash) case Base58.Prefix.PubkeyAddressTestnet => FallbackAddress(17.toByte, hash) @@ -269,35 +280,45 @@ object PaymentRequest { } def fromBech32Address(address: String): FallbackAddress = { - val (_, version, hash) = Bech32.decodeWitnessAddress(address) + val (_, version, hash) = { + val decoded = Bech32.decodeWitnessAddress(address) + (decoded.getFirst, decoded.getSecond, ByteVector.view(decoded.getThird)) + } FallbackAddress(version, hash) } def toAddress(f: FallbackAddress, prefix: String): String = { import f.data f.version match { - case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data) - case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data) - case 17 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data) - case 18 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data) - case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data) - case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data) - case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data) + case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data.toArray) + case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data.toArray) + case 17 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data.toArray) + case 18 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data.toArray) + case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data.toArray) + case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data.toArray) + case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data.toArray) } } } /** - * This returns a bitvector with the minimum size necessary to encode the long, left padded - * to have a length (in bits) multiples of 5 + * This returns a bitvector with the minimum size necessary to encode the long, left padded to have a length (in bits) + * that is a multiple of 5. + */ + def long2bits(l: Long): BitVector = leftPaddedBits(BitVector.fromLong(l)) + + /** + * This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in + * bits) that is a multiple of 5. */ - def long2bits(l: Long) = { - val bin = BitVector.fromLong(l) + def features2bits[T <: Feature](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits) + + private def leftPaddedBits(bits: BitVector): BitVector = { var highest = -1 - for (i <- 0 until bin.size.toInt) { - if (highest == -1 && bin(i)) highest = i + for (i <- 0 until bits.size.toInt) { + if (highest == -1 && bits(i)) highest = i } - val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest) + val nonPadded = if (highest == -1) BitVector.empty else bits.drop(highest) nonPadded.size % 5 match { case 0 => nonPadded case remaining => BitVector.fill(5 - remaining)(high = false) ++ nonPadded @@ -331,7 +352,7 @@ object PaymentRequest { object Expiry { /** - * @param seconds expiry data for this payment request + * @param seconds expiry data for this invoice */ def apply(seconds: Long): Expiry = Expiry(long2bits(seconds)) } @@ -355,25 +376,7 @@ object PaymentRequest { /** * Features supported or required for receiving this payment. */ - case class PaymentRequestFeatures(bitmask: BitVector) extends TaggedField { - lazy val features: Features = Features(bitmask) - lazy val allowMultiPart: Boolean = features.hasFeature(Features.BasicMultiPartPayment) - lazy val allowPaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret) - lazy val requirePaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)) - lazy val allowTrampoline: Boolean = features.hasFeature(Features.TrampolinePayment) - - def toByteVector: ByteVector = features.toByteVector - - def areSupported(nodeParams: NodeParams): Boolean = nodeParams.features.areSupported(features) - - override def toString: String = s"Features(${bitmask.toBin})" - } - - object PaymentRequestFeatures { - def apply(features: Map[Feature with InvoiceFeature, FeatureSupport]): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) { - case (current, (feature, support)) => current + (1L << feature.supportBit(support)) - })) - } + case class InvoiceFeatures(features: Features[Feature]) extends TaggedField object Codecs { @@ -416,7 +419,7 @@ object PaymentRequest { .typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(4, dataCodec(bits).as[UnknownTag4]) - .typecase(5, dataCodec(bits).as[PaymentRequestFeatures]) + .typecase(5, dataCodec(bits).xmap[Features[Feature]](Features(_), features2bits).as[InvoiceFeatures]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) @@ -509,15 +512,15 @@ object PaymentRequest { // TODO: could be optimized by preallocating the resulting buffer def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _) - val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5)) + val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b)) /** - * @param input bech32-encoded payment request - * @return a payment request + * @param input bech32-encoded invoice + * @return a Bolt11 invoice */ - def read(input: String): PaymentRequest = { + def fromString(input: String): Try[Bolt11Invoice] = Try { // used only for data validation - Bech32.decode(input) + Bech32.decode(input, false) val lowercaseInput = input.toLowerCase val separatorIndex = lowercaseInput.lastIndexOf('1') val hrp = lowercaseInput.take(separatorIndex) @@ -534,66 +537,21 @@ object PaymentRequest { case Success(value) => value case Failure(e) => throw e } - PaymentRequest( + Bolt11Invoice( prefix = prefix, - amount = amount_opt, - timestamp = bolt11Data.timestamp, + amount_opt = amount_opt, + createdAt = bolt11Data.timestamp, nodeId = pub, tags = bolt11Data.taggedFields, signature = bolt11Data.signature) } - private def readBoltData(input: String): Bolt11Data = { - val lowercaseInput = input.toLowerCase - val separatorIndex = lowercaseInput.lastIndexOf('1') - val hrp = lowercaseInput.take(separatorIndex) - if (!prefixes.values.exists(prefix => hrp.startsWith(prefix))) throw new RuntimeException("unknown prefix") - val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size - Codecs.bolt11DataCodec.decode(data).require.value - } - - /** - * Extracts the description from a serialized payment request that is **expected to be valid**. - * Throws an error if the payment request is not valid. - * - * @param input valid serialized payment request - * @return description as a String. If the description is a hash, returns the hash value as a String. - */ - def fastReadDescription(input: String): String = { - readBoltData(input).taggedFields.collectFirst { - case PaymentRequest.Description(d) => d - case PaymentRequest.DescriptionHash(h) => h.toString() - }.get - } - - /** - * Checks if a serialized payment request is expired. Timestamp is compared to the System's current time. - * - * @param input valid serialized payment request - * @return true if the payment request has expired, false otherwise. - */ - def fastHasExpired(input: String): Boolean = { - val bolt11Data = readBoltData(input) - val expiry_opt = bolt11Data.taggedFields.collectFirst { - case p: PaymentRequest.Expiry => p - } - val timestamp = bolt11Data.timestamp - expiry_opt match { - case Some(expiry) => timestamp + expiry.toLong <= TimestampSecond.now() - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() + def toExtraEdges(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[Invoice.ExtraEdge] = { + // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination + val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId + extraRoute.zip(nextNodeIds).map { + case (extraHop, nextNodeId) => + Invoice.BasicEdge(extraHop.nodeId, nextNodeId, extraHop.shortChannelId, extraHop.feeBase, extraHop.feeProportionalMillionths, extraHop.cltvExpiryDelta) } } - - /** - * @param pr payment request - * @return a bech32-encoded payment request - */ - def write(pr: PaymentRequest): String = { - // currency unit is Satoshi, but we compute amounts in Millisatoshis - val hramount = Amount.encode(pr.amount) - val hrp = s"${pr.prefix}$hramount" - val data = Codecs.bolt11DataCodec.encode(Bolt11Data(pr.timestamp, pr.tags, pr.signature)).require - val int5s = eight2fiveCodec.decode(data).require.value - Bech32.encode(hrp, int5s.toArray) - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala new file mode 100644 index 0000000000..d62bde5b63 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -0,0 +1,194 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment + +import fr.acinq.bitcoin.Bech32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.wire.protocol.{OfferTypes, TlvStream} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, randomKey, MilliSatoshiLong} +import scodec.bits.ByteVector + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration +import scala.util.Try + +/** + * Lightning Bolt 12 invoice + * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + */ +case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { + + import Bolt12Invoice._ + + require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount") + require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id") + require(records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 invoices must provide a blinded path") + require(records.get[PaymentPathsInfo].exists(_.paymentInfo.length == records.get[Paths].get.paths.length), "bolt 12 invoices must provide a blinded_payinfo for each path") + require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash") + require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description") + require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp") + require(records.get[Signature].nonEmpty, "bolt 12 invoices must provide a signature") + + val amount: MilliSatoshi = records.get[Amount].map(_.amount).get + + override val amount_opt: Option[MilliSatoshi] = Some(amount) + + override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey + + override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash + + override val paymentSecret: Option[ByteVector32] = None + + override val paymentMetadata: Option[ByteVector] = None + + override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description) + + override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty // TODO: the blinded paths need to be converted to graph edges + + override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp + + override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) + + override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA) + + override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + override def toString: String = { + val data = invoiceCodec.encode(this).require.bytes + Bech32.encodeBytes(hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) + + val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId) + + val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths + + val issuer: Option[String] = records.get[Issuer].map(_.issuer) + + val quantity: Option[Long] = records.get[Quantity].map(_.quantity) + + val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash) + + val payerKey: Option[ByteVector32] = records.get[PayerKey].map(_.publicKey) + + val payerNote: Option[String] = records.get[PayerNote].map(_.note) + + val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info) + + val fallbacks: Option[Seq[FallbackAddress]] = records.get[Fallbacks].map(_.addresses) + + val refundSignature: Option[ByteVector64] = records.get[RefundSignature].map(_.signature) + + val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash) + + val signature: ByteVector64 = records.get[Signature].get.signature + + // It is assumed that the request is valid for this offer. + def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = { + nodeId == offer.nodeId && + checkSignature() && + offerId.contains(request.offerId) && + request.chain == chain && + !isExpired() && + request.amount.contains(amount) && + quantity == request.quantity_opt && + payerKey.contains(request.payerKey) && + payerInfo == request.payerInfo && + // Bolt 12: MUST reject the invoice if payer_note is set, and was unset or not equal to the field in the invoice_request. + payerNote.forall(request.payerNote.contains(_)) && + description.swap.exists(_.startsWith(offer.description)) && + issuer == offer.issuer && + request.features.areSupported(features) + } + + def checkRefundSignature(): Boolean = { + (refundSignature, refundFor, payerKey) match { + case (Some(sig), Some(hash), Some(key)) => verifySchnorr(signatureTag("payer_signature"), hash, sig, key) + case _ => false + } + } + + def checkSignature(): Boolean = { + verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(nodeId)) + } +} + +object Bolt12Invoice { + val hrp = "lni" + val DEFAULT_EXPIRY_SECONDS: Long = 7200 + val DEFAULT_MIN_FINAL_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) + + /** + * Creates an invoice for a given offer and invoice request. + * + * @param offer the offer this invoice corresponds to + * @param request the request this invoice responds to + * @param preimage the preimage to use for the payment + * @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route + * @param features invoice features + * @param selfId data to identify the payment when receiving it, can contain anything and is not readable by the payer + */ + def apply(offer: Offer, + request: InvoiceRequest, + preimage: ByteVector32, + nodeKey: PrivateKey, + minFinalCltvExpiryDelta: CltvExpiryDelta, + features: Features[InvoiceFeature], + selfId: ByteVector): Bolt12Invoice = { + require(request.amount.nonEmpty || offer.amount.nonEmpty) + val amount = request.amount.orElse(offer.amount.map(_ * request.quantity)).get + val tlvs: Seq[InvoiceTlv] = Seq( + Some(Chain(request.chain)), + Some(OfferId(offer.offerId)), + Some(Amount(amount)), + Some(Description(offer.description)), + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, + Some(Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, selfId)))), + Some(PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty)))), + offer.issuer.map(Issuer), + Some(NodeId(nodeKey.publicKey)), + request.quantity_opt.map(Quantity), + Some(PayerKey(request.payerKey)), + request.payerNote.map(PayerNote), + Some(CreatedAt(TimestampSecond.now())), + Some(PaymentHash(Crypto.sha256(preimage))), + Some(Cltv(minFinalCltvExpiryDelta)), + request.payerInfo.map(PayerInfo), + request.replaceInvoice.map(ReplaceInvoice), + ).flatten + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + + def signatureTag(fieldName: String): String = "lightning" + "invoice" + fieldName + + def fromString(input: String): Try[Bolt12Invoice] = Try { + val triple = Bech32.decodeBytes(input.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + invoiceCodec.decode(ByteVector(encoded).bits).require.value + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala new file mode 100644 index 0000000000..ccb9741c27 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.payment.DummyInvoice.dummyInvoiceCodec +import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond} +import scodec.Codec +import scodec.bits.ByteVector + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration +import scala.util.Try + +trait Invoice { + val amount_opt: Option[MilliSatoshi] + + val createdAt: TimestampSecond + + val nodeId: PublicKey + + val paymentHash: ByteVector32 + + val paymentSecret: Option[ByteVector32] + + val paymentMetadata: Option[ByteVector] + + val description: Either[String, ByteVector32] + + val extraEdges: Seq[Invoice.ExtraEdge] + + val relativeExpiry: FiniteDuration + + val minFinalCltvExpiryDelta: CltvExpiryDelta + + val features: Features[InvoiceFeature] + + def isExpired(): Boolean = createdAt + relativeExpiry.toSeconds <= TimestampSecond.now() + + def toString: String +} + +object Invoice { + sealed trait ExtraEdge { + val sourceNodeId: PublicKey + val feeBase: MilliSatoshi + val feeProportionalMillionths: Long + val cltvExpiryDelta: CltvExpiryDelta + val htlcMinimum: MilliSatoshi + val htlcMaximum_opt: Option[MilliSatoshi] + + def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths) + } + + case class BasicEdge(sourceNodeId: PublicKey, + targetNodeId: PublicKey, + shortChannelId: ShortChannelId, + feeBase: MilliSatoshi, + feeProportionalMillionths: Long, + cltvExpiryDelta: CltvExpiryDelta) extends ExtraEdge { + override val htlcMinimum: MilliSatoshi = 0 msat + override val htlcMaximum_opt: Option[MilliSatoshi] = None + + def update(u: ChannelUpdate): BasicEdge = copy(feeBase = u.feeBaseMsat, feeProportionalMillionths = u.feeProportionalMillionths, cltvExpiryDelta = u.cltvExpiryDelta) + } + + def fromString(input: String): Try[Invoice] = { + if (input.toLowerCase.startsWith("lni")) { + Bolt12Invoice.fromString(input) + } else if (input.startsWith("lnd")) { + DummyInvoice.fromString(input) + } else { + Bolt11Invoice.fromString(input) + } + } +} + +/** Dummy invoice used for unsolicited payments + */ +case class DummyInvoice(amount: MilliSatoshi, + createdAt: TimestampSecond, + nodeId: PublicKey, + paymentHash: ByteVector32, + paymentSecret: Option[ByteVector32], + minFinalCltvExpiryDelta: CltvExpiryDelta, + features: Features[InvoiceFeature]) extends Invoice { + override val amount_opt: Option[MilliSatoshi] = Some(amount) + override val paymentMetadata: Option[ByteVector] = None + override val description: Either[String, ByteVector32] = Left("Donation") + override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty + override val relativeExpiry: FiniteDuration = FiniteDuration(Bolt11Invoice.DEFAULT_EXPIRY_SECONDS, TimeUnit.SECONDS) + + override def toString: String = { + "lnd" + dummyInvoiceCodec.encode(this).require.bytes.toBase64 + } +} + +object DummyInvoice { + + import fr.acinq.eclair.wire.protocol.CommonCodecs._ + import scodec.codecs._ + + private val dummyInvoiceCodec: Codec[DummyInvoice] = ( + ("amount" | millisatoshi) :: + ("createdAt" | timestampSecond) :: + ("nodeId" | publicKey) :: + ("paymentHash" | bytes32) :: + ("paymentSecret" | optional(bool8, bytes32)) :: + ("minFinalCltvExpiryDelta" | cltvExpiryDelta) :: + ("features" | bytes.xmap[Features[InvoiceFeature]](Features(_).invoiceFeatures(), _.toByteVector))).as[DummyInvoice] + + def fromString(input: String): Try[DummyInvoice] = Try { + dummyInvoiceCodec.decodeValue(ByteVector.fromValidBase64(input.stripPrefix("lnd")).bits).require + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala index 3772216e5e..13a86cd511 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.channel.CMD_FAIL_HTLC import kamon.Kamon diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 933b4bfbf7..226ad8544c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,10 +16,11 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} +import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} @@ -154,6 +155,7 @@ object PaymentFailure { def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { failures match { case previousFailures :+ LocalFailure(_, _, RouteNotFound) if previousFailures.nonEmpty => previousFailures + case previousFailures :+ LocalFailure(_, _, RetryExhausted) if previousFailures.nonEmpty => previousFailures case other => other } } @@ -180,7 +182,7 @@ object PaymentFailure { /** Ignore the channel outgoing from the given nodeId in the given route. */ private def ignoreNodeOutgoingChannel(nodeId: PublicKey, hops: Seq[Hop], ignore: Ignore): Ignore = { hops.collectFirst { - case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId) } match { case Some(faultyChannel) => ignore + faultyChannel case None => ignore @@ -219,7 +221,7 @@ object PaymentFailure { ignore ++ blacklist case LocalFailure(_, hops, _) => hops.headOption match { case Some(hop: ChannelHop) => - val faultyChannel = ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + val faultyChannel = ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId) ignore + faultyChannel case _ => ignore } @@ -231,7 +233,7 @@ object PaymentFailure { } /** Update the invoice routing hints based on more recent channel updates received. */ - def updateRoutingHints(failures: Seq[PaymentFailure], routingHints: Seq[Seq[ExtraHop]]): Seq[Seq[ExtraHop]] = { + def updateExtraEdges(failures: Seq[PaymentFailure], extraEdges: Seq[ExtraEdge]): Seq[ExtraEdge] = { // We're only interested in the last channel update received per channel. val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) { case (current, failure) => failure match { @@ -239,23 +241,24 @@ object PaymentFailure { case _ => current } } - routingHints.map(_.map(extraHop => updates.get(extraHop.shortChannelId) match { - case Some(u) => extraHop.copy( - cltvExpiryDelta = u.cltvExpiryDelta, - feeBase = u.feeBaseMsat, - feeProportionalMillionths = u.feeProportionalMillionths - ) - case None => extraHop - })) + extraEdges.map { + case edge: BasicEdge => updates.get(edge.shortChannelId) match { + case Some(u) => edge.update(u) + case None => edge + } + case edge => edge + } } } -case class PathFindingExperimentMetrics(amount: MilliSatoshi, +case class PathFindingExperimentMetrics(paymentHash: ByteVector32, + amount: MilliSatoshi, fees: MilliSatoshi, status: String, duration: FiniteDuration, timestamp: TimestampMilli, isMultiPart: Boolean, experimentName: String, - recipientNodeId: PublicKey) + recipientNodeId: PublicKey, + routingHints_opt: Option[Seq[Seq[Bolt11Invoice.ExtraHop]]]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index f761c625eb..8f42f672e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} @@ -159,7 +159,7 @@ object OutgoingPaymentPacket { /** * Build the onion payloads for each hop. * - * @param hops the hops as computed by the router + extra routes from payment request + * @param hops the hops as computed by the router + extra routes from the invoice * @param finalPayload payload data for the final node (amount, expiry, etc) * @return a (firstAmount, firstExpiry, payloads) tuple where: * - firstAmount is the amount for the first htlc in the route @@ -170,7 +170,7 @@ object OutgoingPaymentPacket { hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](finalPayload))) { case ((amount, expiry, payloads), hop) => val payload = hop match { - case hop: ChannelHop => PaymentOnion.ChannelRelayTlvPayload(hop.lastUpdate.shortChannelId, amount, expiry) + case hop: ChannelHop => PaymentOnion.ChannelRelayTlvPayload(hop.shortChannelId, amount, expiry) case hop: NodeHop => PaymentOnion.createNodeRelayPayload(amount, expiry, hop.nextNodeId) } (amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, payload +: payloads) @@ -180,7 +180,7 @@ object OutgoingPaymentPacket { /** * Build an encrypted onion packet with the given final payload. * - * @param hops the hops as computed by the router + extra routes from payment request, including ourselves in the first hop + * @param hops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop * @param finalPayload payload data for the final node (amount, expiry, etc) * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the first htlc in the route @@ -212,7 +212,7 @@ object OutgoingPaymentPacket { * - firstExpiry is the cltv expiry for the first trampoline node in the route * - the trampoline onion to include in final payload of a normal onion */ - def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + def buildTrampolineToLegacyPacket(invoice: Bolt11Invoice, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { // NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment. // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala new file mode 100644 index 0000000000..28d96061cf --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment.receive + +import akka.actor.typed.Behavior +import akka.actor.typed.eventstream.EventStream +import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import fr.acinq.eclair.db.IncomingPaymentsDb +import fr.acinq.eclair.payment.receive.InvoicePurger.{Command, PurgeCompleted, TickPurge} +import fr.acinq.eclair.{TimestampMilli, TimestampMilliLong} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} + +/** + * This actor will purge expired invoices from the database it was initialized with at a scheduled interval. + * At startup scan the entire database for expired invoices, subsequent runs only look back 15 days. + */ +object InvoicePurger { + + def apply(paymentsDb: IncomingPaymentsDb, interval: FiniteDuration): Behavior[Command] = + Behaviors.setup { context => + // wait for purge events sent at `interval` + Behaviors.withTimers { timers => + timers.startTimerAtFixedRate(TickPurge, initialDelay = 0.seconds, interval = interval) + new InvoicePurger(paymentsDb, context).waiting(true) + } + } + + sealed trait Command + + sealed trait PurgeEvent + + // this notification is sent when we have completed our invoice purge process + case object PurgeCompleted extends PurgeEvent + + private case object TickPurge extends Command +} + +class InvoicePurger private(paymentsDb: IncomingPaymentsDb, context: ActorContext[Command]) { + + // purge at each tick unless currently purging + def waiting(fullScan: Boolean): Behavior[Command] = + Behaviors.receiveMessage { + case TickPurge => + val now = TimestampMilli.now() + val start = if (fullScan) 0 unixms else now - 15.days + val expiredPayments = paymentsDb.listExpiredIncomingPayments(start, now) + // purge expired payments + expiredPayments.foreach(p => paymentsDb.removeIncomingPayment(p.invoice.paymentHash)) + + // publish a notification when we have purged expired invoices + if (expiredPayments.nonEmpty) { + context.system.eventStream ! EventStream.Publish(PurgeCompleted) + } + waiting(fullScan = false) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 896b0fd5df..45dabd7001 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -22,20 +22,22 @@ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{ActorContext, ActorRef, PoisonPill, Status} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} -import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, PaymentRequest} +import fr.acinq.eclair.payment.{Bolt11Invoice, DummyInvoice, IncomingPaymentPacket, Invoice, PaymentMetadataReceived, PaymentReceived} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Feature, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32} +import fr.acinq.eclair.{FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32} import scodec.bits.HexStringSyntax +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} /** - * Simple payment handler that generates payment requests and fulfills incoming htlcs. + * Simple payment handler that generates invoices and fulfills incoming htlcs. * * Created by PM on 17/06/2016. */ @@ -58,7 +60,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { case receivePayment: ReceivePayment => val child = ctx.spawnAnonymous(CreateInvoiceActor(nodeParams)) - child ! CreateInvoiceActor.CreatePaymentRequest(ctx.sender(), receivePayment) + child ! CreateInvoiceActor.CreateInvoice(ctx.sender(), receivePayment) case p: IncomingPaymentPacket.FinalPacket if doHandle(p.add.paymentHash) => val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams)) @@ -89,18 +91,17 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } case None => p.payload.paymentPreimage match { case Some(paymentPreimage) if nodeParams.features.hasFeature(Features.KeySend) => - val amount = Some(p.payload.totalAmount) + val amount = p.payload.totalAmount val paymentHash = Crypto.sha256(paymentPreimage) - val desc = Left("Donation") - val features: Map[Feature with InvoiceFeature, FeatureSupport] = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { - Map(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { + Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } else { - Map(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler - val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features)) - log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount) - db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend) + val invoice = DummyInvoice(amount, TimestampSecond.now(), nodeParams.privateKey.publicKey, paymentHash, p.payload.paymentSecret, nodeParams.channelConf.minFinalExpiryDelta, features) + log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) + db.addIncomingPayment(invoice, paymentPreimage, paymentType = PaymentType.KeySend) ctx.self ! p case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() @@ -217,42 +218,42 @@ object MultiPartHandler { // @formatter:off sealed trait Command - case class CreatePaymentRequest(replyTo: ActorRef, receivePayment: ReceivePayment) extends Command + case class CreateInvoice(replyTo: ActorRef, receivePayment: ReceivePayment) extends Command // @formatter:on def apply(nodeParams: NodeParams): Behavior[Command] = { Behaviors.setup { context => Behaviors.receiveMessage { - case CreatePaymentRequest(replyTo, receivePayment) => + case CreateInvoice(replyTo, receivePayment) => Try { import receivePayment._ val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32()) val paymentHash = Crypto.sha256(paymentPreimage) - val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) + val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds) val paymentMetadata = hex"2a" val invoiceFeatures = if (nodeParams.enableTrampolinePayment) { - nodeParams.features.invoiceFeatures() + (Features.TrampolinePayment -> FeatureSupport.Optional) + Features[InvoiceFeature](nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePaymentPrototype -> FeatureSupport.Optional)) } else { nodeParams.features.invoiceFeatures() } - val paymentRequest = PaymentRequest( + val invoice = Bolt11Invoice( nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, - nodeParams.minFinalExpiryDelta, + nodeParams.channelConf.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, paymentMetadata = Some(paymentMetadata), - features = PaymentRequestFeatures(invoiceFeatures) + features = invoiceFeatures ) - context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) - nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType) - paymentRequest + context.log.debug("generated invoice={} from amount={}", invoice.toString, amount_opt) + nodeParams.db.payments.addIncomingPayment(invoice, paymentPreimage, paymentType) + invoice } match { - case Success(paymentRequest) => replyTo ! paymentRequest + case Success(invoice) => replyTo ! invoice case Failure(exception) => replyTo ! Status.Failure(exception) } Behaviors.stopped @@ -281,7 +282,7 @@ object MultiPartHandler { if (record.status.isInstanceOf[IncomingPaymentStatus.Received]) { log.warning("ignoring incoming payment for which has already been paid") false - } else if (record.paymentRequest.isExpired) { + } else if (record.invoice.isExpired()) { log.warning("received payment for expired amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false } else { @@ -305,7 +306,7 @@ object MultiPartHandler { } private def validatePaymentCltv(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = { - val minExpiry = record.paymentRequest.minFinalCltvExpiryDelta.getOrElse(nodeParams.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) + val minExpiry = record.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight) if (payment.add.cltvExpiry < minExpiry) { log.warning("received payment with expiry too small for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false @@ -314,14 +315,14 @@ object MultiPartHandler { } } - private def validateInvoiceFeatures(payment: IncomingPaymentPacket.FinalPacket, pr: PaymentRequest)(implicit log: LoggingAdapter): Boolean = { - if (payment.payload.amount < payment.payload.totalAmount && !pr.features.allowMultiPart) { + private def validateInvoiceFeatures(payment: IncomingPaymentPacket.FinalPacket, invoice: Invoice)(implicit log: LoggingAdapter): Boolean = { + if (payment.payload.amount < payment.payload.totalAmount && !invoice.features.hasFeature(Features.BasicMultiPartPayment)) { log.warning("received multi-part payment but invoice doesn't support it for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false - } else if (payment.payload.amount < payment.payload.totalAmount && !pr.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (payment.payload.amount < payment.payload.totalAmount && invoice.paymentSecret != payment.payload.paymentSecret) { log.warning("received multi-part payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false - } else if (!pr.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (invoice.paymentSecret != payment.payload.paymentSecret) { log.warning("received payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false } else { @@ -332,10 +333,10 @@ object MultiPartHandler { private def validatePayment(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. val cmdFail = CMD_FAIL_HTLC(payment.add.id, Right(IncorrectOrUnknownPaymentDetails(payment.payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) - val paymentAmountOk = record.paymentRequest.amount.forall(a => validatePaymentAmount(payment, a)) + val paymentAmountOk = record.invoice.amount_opt.forall(a => validatePaymentAmount(payment, a)) val paymentCltvOk = validatePaymentCltv(nodeParams, payment, record) val paymentStatusOk = validatePaymentStatus(payment, record) - val paymentFeaturesOk = validateInvoiceFeatures(payment, record.paymentRequest) + val paymentFeaturesOk = validateInvoiceFeatures(payment, record.invoice) if (paymentAmountOk && paymentCltvOk && paymentStatusOk && paymentFeaturesOk) None else Some(cmdFail) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index e6babcabfa..4d5fca27f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment.receive import akka.actor.{ActorRef, Props} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{FailureMessage, IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala index f045a3a904..90a892eb3b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala @@ -17,6 +17,9 @@ package fr.acinq.eclair.payment.receive import akka.actor.Actor.Receive +import akka.actor.typed.SupervisorStrategy +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorContext, ActorRef, DiagnosticActorLogging, Props} import akka.event.DiagnosticLoggingAdapter import akka.event.Logging.MDC @@ -34,6 +37,14 @@ class PaymentHandler(nodeParams: NodeParams, register: ActorRef) extends Actor w // we do this instead of sending it to ourselves, otherwise there is no guarantee that this would be the first processed message private val defaultHandler = new MultiPartHandler(nodeParams, register, nodeParams.db.payments) + // Spawn an actor to purge expired invoices at a configured interval + private val purger = nodeParams.purgeInvoicesInterval match { + case Some(interval) => + context.spawn(Behaviors.supervise(InvoicePurger(nodeParams.db.payments, interval)).onFailure(SupervisorStrategy.restart), name = "purge-expired-invoices") + case _ => + log.warning("purge-expired-invoices is disabled") + } + override def receive: Receive = normal(defaultHandler.handle(context, log)) private def addReceiveHandler(handle: Receive): Receive = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 5f37a435fb..3ba8c507f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -21,14 +21,14 @@ import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.relay.Relayer.OutgoingChannel +import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams} import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, channel, nodeFee} +import fr.acinq.eclair.{Logs, NodeParams, TimestampSecond, channel, nodeFee} import java.util.UUID @@ -37,22 +37,23 @@ object ChannelRelay { // @formatter:off sealed trait Command private case object DoRelay extends Command - private case class WrappedForwardShortIdFailure(failure: Register.ForwardShortIdFailure[CMD_ADD_HTLC]) extends Command + private case class WrappedForwardFailure(failure: Register.ForwardFailure[CMD_ADD_HTLC]) extends Command private case class WrappedAddResponse(res: CommandResponse[CMD_ADD_HTLC]) extends Command // @formatter:on // @formatter:off sealed trait RelayResult case class RelayFailure(cmdFail: CMD_FAIL_HTLC) extends RelayResult - case class RelaySuccess(shortChannelId: ShortChannelId, cmdAdd: CMD_ADD_HTLC) extends RelayResult + case class RelaySuccess(selectedChannelId: ByteVector32, cmdAdd: CMD_ADD_HTLC) extends RelayResult // @formatter:on - def apply(nodeParams: NodeParams, register: ActorRef, channels: Map[ShortChannelId, Relayer.OutgoingChannel], relayId: UUID, r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] = + def apply(nodeParams: NodeParams, register: ActorRef, channels: Map[ByteVector32, Relayer.OutgoingChannel], relayId: UUID, r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] = Behaviors.setup { context => Behaviors.withMdc(Logs.mdc( category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(relayId), // for a channel relay, parent payment id = relay id - paymentHash_opt = Some(r.add.paymentHash))) { + paymentHash_opt = Some(r.add.paymentHash), + nodeAlias_opt = Some(nodeParams.alias))) { context.self ! DoRelay new ChannelRelay(nodeParams, register, channels, r, context).relay(Seq.empty) } @@ -92,16 +93,16 @@ object ChannelRelay { */ class ChannelRelay private(nodeParams: NodeParams, register: ActorRef, - channels: Map[ShortChannelId, Relayer.OutgoingChannel], + channels: Map[ByteVector32, Relayer.OutgoingChannel], r: IncomingPaymentPacket.ChannelRelayPacket, context: ActorContext[ChannelRelay.Command]) { import ChannelRelay._ - private val forwardShortIdAdapter = context.messageAdapter[Register.ForwardShortIdFailure[CMD_ADD_HTLC]](WrappedForwardShortIdFailure) + private val forwardFailureAdapter = context.messageAdapter[Register.ForwardFailure[CMD_ADD_HTLC]](WrappedForwardFailure) private val addResponseAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedAddResponse) - private case class PreviouslyTried(shortChannelId: ShortChannelId, failure: RES_ADD_FAILED[ChannelException]) + private case class PreviouslyTried(channelId: ByteVector32, failure: RES_ADD_FAILED[ChannelException]) def relay(previousFailures: Seq[PreviouslyTried]): Behavior[Command] = { Behaviors.receiveMessagePartial { @@ -115,18 +116,18 @@ class ChannelRelay private(nodeParams: NodeParams, Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) context.log.info(s"rejecting htlc reason=${cmdFail.reason}") safeSendAndStop(r.add.channelId, cmdFail) - case RelaySuccess(selectedShortChannelId, cmdAdd) => - context.log.info(s"forwarding htlc to shortChannelId=$selectedShortChannelId") - register ! Register.ForwardShortId(forwardShortIdAdapter.toClassic, selectedShortChannelId, cmdAdd) - waitForAddResponse(selectedShortChannelId, previousFailures) + case RelaySuccess(selectedChannelId, cmdAdd) => + context.log.info(s"forwarding htlc to channelId=$selectedChannelId") + register ! Register.Forward(forwardFailureAdapter, selectedChannelId, cmdAdd) + waitForAddResponse(selectedChannelId, previousFailures) } } } - def waitForAddResponse(selectedShortChannelId: ShortChannelId, previousFailures: Seq[PreviouslyTried]): Behavior[Command] = + def waitForAddResponse(selectedChannelId: ByteVector32, previousFailures: Seq[PreviouslyTried]): Behavior[Command] = Behaviors.receiveMessagePartial { - case WrappedForwardShortIdFailure(Register.ForwardShortIdFailure(Register.ForwardShortId(_, shortChannelId, CMD_ADD_HTLC(_, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) => - context.log.warn(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${o.add.id}") + case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) => + context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}") val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer), commit = true) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(o.add.channelId, cmdFail) @@ -134,7 +135,7 @@ class ChannelRelay private(nodeParams: NodeParams, case WrappedAddResponse(addFailed@RES_ADD_FAILED(CMD_ADD_HTLC(_, _, _, _, _, _: Origin.ChannelRelayedHot, _), _, _)) => context.log.info("attempt failed with reason={}", addFailed.t.getClass.getSimpleName) context.self ! DoRelay - relay(previousFailures :+ PreviouslyTried(selectedShortChannelId, addFailed)) + relay(previousFailures :+ PreviouslyTried(selectedChannelId, addFailed)) case WrappedAddResponse(_: RES_SUCCESS[_]) => context.log.debug("sent htlc to the downstream channel") @@ -170,78 +171,86 @@ class ChannelRelay private(nodeParams: NodeParams, * - a CMD_ADD_HTLC to propagate downstream */ def handleRelay(previousFailures: Seq[PreviouslyTried]): RelayResult = { - val alreadyTried = previousFailures.map(_.shortChannelId) - selectPreferredChannel(alreadyTried) - .flatMap(selectedShortChannelId => channels.get(selectedShortChannelId).map(_.channelUpdate)) match { + val alreadyTried = previousFailures.map(_.channelId) + selectPreferredChannel(alreadyTried) match { case None if previousFailures.nonEmpty => // no more channels to try val error = previousFailures // we return the error for the initially requested channel if it exists - .find(_.shortChannelId == r.payload.outgoingChannelId) + .find(failure => requestedChannelId_opt.contains(failure.channelId)) // otherwise we return the error for the first channel tried .getOrElse(previousFailures.head) .failure RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(translateLocalError(error.t, error.channelUpdate)), commit = true)) - case channelUpdate_opt => - relayOrFail(channelUpdate_opt) + case outgoingChannel_opt => + relayOrFail(outgoingChannel_opt) } } /** all the channels point to the same next node, we take the first one */ private val nextNodeId_opt = channels.headOption.map(_._2.nextNodeId) + /** channel id explicitly requested in the onion payload */ + private val requestedChannelId_opt = channels.collectFirst { + case (channelId, channel) if channel.shortIds.localAlias == r.payload.outgoingChannelId => channelId + case (channelId, channel) if channel.shortIds.real.toOption.contains(r.payload.outgoingChannelId) => channelId + } + /** * Select a channel to the same node to relay the payment to, that has the lowest capacity and balance and is * compatible in terms of fees, expiry_delta, etc. * * If no suitable channel is found we default to the originally requested channel. */ - def selectPreferredChannel(alreadyTried: Seq[ShortChannelId]): Option[ShortChannelId] = { + def selectPreferredChannel(alreadyTried: Seq[ByteVector32]): Option[OutgoingChannel] = { val requestedShortChannelId = r.payload.outgoingChannelId context.log.debug("selecting next channel with requestedShortChannelId={}", requestedShortChannelId) - nextNodeId_opt match { - case Some(_) => - // we then filter out channels that we have already tried - val candidateChannels: Map[ShortChannelId, OutgoingChannel] = channels -- alreadyTried - // and we filter again to keep the ones that are compatible with this payment (mainly fees, expiry delta) - candidateChannels - .map { case (shortChannelId, channelInfo) => - val relayResult = relayOrFail(Some(channelInfo.channelUpdate)) - context.log.debug(s"candidate channel: shortChannelId=$shortChannelId availableForSend={} capacity={} channelUpdate={} result={}", - channelInfo.commitments.availableBalanceForSend, - channelInfo.commitments.capacity, - channelInfo.channelUpdate, - relayResult match { - case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _)) => failureReason - case other => other - }) - (shortChannelId, channelInfo, relayResult) - } - .collect { - // we only keep channels that have enough balance to handle this payment - case (shortChannelId, channelInfo, _: RelaySuccess) if channelInfo.commitments.availableBalanceForSend > r.payload.amountToForward => (shortChannelId, channelInfo.commitments) - } - .toList // needed for ordering - // we want to use the channel with: - // - the lowest available capacity to ensure we keep high-capacity channels for big payments - // - the lowest available balance to increase our incoming liquidity - .sortBy { case (_, commitments) => (commitments.capacity, commitments.availableBalanceForSend) } - .headOption match { - case Some((preferredShortChannelId, commitments)) if preferredShortChannelId != requestedShortChannelId => - context.log.info("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId, preferredShortChannelId, commitments.availableBalanceForSend) - Some(preferredShortChannelId) - case Some(_) => - context.log.debug("requested short channel id is our preferred channel") - Some(requestedShortChannelId) - case None if !alreadyTried.contains(requestedShortChannelId) => - context.log.debug("no channel seems to work for this payment, we will try to use the requested short channel id") - Some(requestedShortChannelId) - case None => + // we filter out channels that we have already tried + val candidateChannels: Map[ByteVector32, OutgoingChannel] = channels -- alreadyTried + // and we filter again to keep the ones that are compatible with this payment (mainly fees, expiry delta) + candidateChannels + .values + .map { channel => + val relayResult = relayOrFail(Some(channel)) + context.log.debug(s"candidate channel: channelId=${channel.channelId} availableForSend={} capacity={} channelUpdate={} result={}", + channel.commitments.availableBalanceForSend, + channel.commitments.capacity, + channel.channelUpdate, + relayResult match { + case _: RelaySuccess => "success" + case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _)) => failureReason + case other => other + }) + (channel, relayResult) + } + .collect { + // we only keep channels that have enough balance to handle this payment + case (channel, _: RelaySuccess) if channel.commitments.availableBalanceForSend > r.payload.amountToForward => channel + } + .toList // needed for ordering + // we want to use the channel with: + // - the lowest available capacity to ensure we keep high-capacity channels for big payments + // - the lowest available balance to increase our incoming liquidity + .sortBy { channel => (channel.commitments.capacity, channel.commitments.availableBalanceForSend) } + .headOption match { + case Some(channel) => + if (requestedChannelId_opt.contains(channel.channelId)) { + context.log.debug("requested short channel id is our preferred channel") + Some(channel) + } else { + context.log.info("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId, channel.channelUpdate.shortChannelId, channel.commitments.availableBalanceForSend) + Some(channel) + } + case None => + val requestedChannel_opt = requestedChannelId_opt.flatMap(channels.get) + requestedChannel_opt match { + case Some(requestedChannel) if alreadyTried.contains(requestedChannel.channelId) => context.log.debug("no channel seems to work for this payment and we have already tried the requested channel id: giving up") None + case _ => + context.log.debug("no channel seems to work for this payment, we will try to use the one requested") + requestedChannel_opt } - case _ => Some(requestedShortChannelId) // we don't have a channel_update for this short_channel_id } } @@ -250,22 +259,25 @@ class ChannelRelay private(nodeParams: NodeParams, * channel, because some parameters don't match with our settings for that channel. In that case we directly fail the * htlc. */ - def relayOrFail(channelUpdate_opt: Option[ChannelUpdate]): RelayResult = { + def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = { import r._ - channelUpdate_opt match { + outgoingChannel_opt match { case None => RelayFailure(CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) - case Some(channelUpdate) if !channelUpdate.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)), commit = true)) - case Some(channelUpdate) if payload.amountToForward < channelUpdate.htlcMinimumMsat => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.expiryDelta < channelUpdate.cltvExpiryDelta => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate, payload.amountToForward) => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) - case Some(channelUpdate) => + case Some(c) if !c.channelUpdate.channelFlags.isEnabled => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true)) + case Some(c) if payload.amountToForward < c.channelUpdate.htlcMinimumMsat => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, c.channelUpdate)), commit = true)) + case Some(c) if r.expiryDelta < c.channelUpdate.cltvExpiryDelta => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, c.channelUpdate)), commit = true)) + case Some(c) if r.relayFeeMsat < nodeFee(c.channelUpdate.relayFees, payload.amountToForward) && + // fees also do not satisfy the previous channel update for `enforcementDelay` seconds after current update + (TimestampSecond.now() - c.channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || + outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, payload.amountToForward))) => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, c.channelUpdate)), commit = true)) + case Some(c: OutgoingChannel) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, add, payload.amountToForward) - RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, origin, commit = true)) + RelaySuccess(c.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, origin, commit = true)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 2ba915549b..59eb7b58b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -16,18 +16,17 @@ package fr.acinq.eclair.payment.relay -import java.util.UUID - import akka.actor.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket -import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId} +import fr.acinq.eclair.{SubscriptionsComplete, Logs, NodeParams, ShortChannelId} +import java.util.UUID import scala.collection.mutable /** @@ -47,85 +46,79 @@ object ChannelRelayer { private[payment] case class WrappedLocalChannelUpdate(localChannelUpdate: LocalChannelUpdate) extends Command private[payment] case class WrappedLocalChannelDown(localChannelDown: LocalChannelDown) extends Command private[payment] case class WrappedAvailableBalanceChanged(availableBalanceChanged: AvailableBalanceChanged) extends Command - private[payment] case class WrappedShortChannelIdAssigned(shortChannelIdAssigned: ShortChannelIdAssigned) extends Command // @formatter:on def mdc: Command => Map[String, String] = { case c: Relay => Logs.mdc(paymentHash_opt = Some(c.channelRelayPacket.add.paymentHash)) + case c: WrappedLocalChannelUpdate => Logs.mdc(channelId_opt = Some(c.localChannelUpdate.channelId)) + case c: WrappedLocalChannelDown => Logs.mdc(channelId_opt = Some(c.localChannelDown.channelId)) + case c: WrappedAvailableBalanceChanged => Logs.mdc(channelId_opt = Some(c.availableBalanceChanged.channelId)) case _ => Map.empty } - private type ChannelUpdates = Map[ShortChannelId, Relayer.OutgoingChannel] - private type NodeChannels = mutable.MultiDict[PublicKey, ShortChannelId] - def apply(nodeParams: NodeParams, register: ActorRef, - channelUpdates: ChannelUpdates = Map.empty, - node2channels: NodeChannels = mutable.MultiDict.empty[PublicKey, ShortChannelId]): Behavior[Command] = + channels: Map[ByteVector32, Relayer.OutgoingChannel] = Map.empty, + scid2channels: Map[ShortChannelId, ByteVector32] = Map.empty, + node2channels: mutable.MultiDict[PublicKey, ByteVector32] = mutable.MultiDict.empty): Behavior[Command] = Behaviors.setup { context => context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelUpdate](WrappedLocalChannelUpdate)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelDown](WrappedLocalChannelDown)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[AvailableBalanceChanged](WrappedAvailableBalanceChanged)) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ShortChannelIdAssigned](WrappedShortChannelIdAssigned)) + context.system.eventStream ! EventStream.Publish(SubscriptionsComplete(this.getClass)) context.messageAdapter[IncomingPaymentPacket.ChannelRelayPacket](Relay) - Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) { + Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), nodeAlias_opt = Some(nodeParams.alias)), mdc) { Behaviors.receiveMessage { case Relay(channelRelayPacket) => val relayId = UUID.randomUUID() - val nextNodeId_opt: Option[PublicKey] = channelUpdates.get(channelRelayPacket.payload.outgoingChannelId) match { - case Some(channel) => Some(channel.nextNodeId) + val nextNodeId_opt: Option[PublicKey] = scid2channels.get(channelRelayPacket.payload.outgoingChannelId) match { + case Some(channelId) => channels.get(channelId).map(_.nextNodeId) case None => None } - val channels: Map[ShortChannelId, Relayer.OutgoingChannel] = nextNodeId_opt match { - case Some(nextNodeId) => node2channels.get(nextNodeId).map(channelUpdates).map(c => c.channelUpdate.shortChannelId -> c).toMap + val nextChannels: Map[ByteVector32, Relayer.OutgoingChannel] = nextNodeId_opt match { + case Some(nextNodeId) => node2channels.get(nextNodeId).flatMap(channels.get).map(c => c.channelId -> c).toMap case None => Map.empty } - context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), channels.keys.mkString(",")) - context.spawn(ChannelRelay.apply(nodeParams, register, channels, relayId, channelRelayPacket), name = relayId.toString) + context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), nextChannels.keys.mkString(",")) + context.spawn(ChannelRelay.apply(nodeParams, register, nextChannels, relayId, channelRelayPacket), name = relayId.toString) Behaviors.same case GetOutgoingChannels(replyTo, Relayer.GetOutgoingChannels(enabledOnly)) => - val channels = if (enabledOnly) { - channelUpdates.values.filter(o => o.channelUpdate.channelFlags.isEnabled) + val selected = if (enabledOnly) { + channels.values.filter(o => o.channelUpdate.channelFlags.isEnabled) } else { - channelUpdates.values + channels.values } - replyTo ! Relayer.OutgoingChannels(channels.toSeq) + replyTo ! Relayer.OutgoingChannels(selected.toSeq) Behaviors.same - case WrappedLocalChannelUpdate(LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments)) => - context.log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) - val channelUpdates1 = channelUpdates + (channelUpdate.shortChannelId -> Relayer.OutgoingChannel(remoteNodeId, channelUpdate, commitments)) - val node2channels1 = node2channels.addOne(remoteNodeId, channelUpdate.shortChannelId) - apply(nodeParams, register, channelUpdates1, node2channels1) + case WrappedLocalChannelUpdate(lcu@LocalChannelUpdate(_, channelId, shortIds, remoteNodeId, _, channelUpdate, commitments)) => + context.log.debug(s"updating local channel info for channelId=$channelId realScid=${shortIds.real} localAlias=${shortIds.localAlias} remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) + val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate) + val channel = Relayer.OutgoingChannel(shortIds, remoteNodeId, channelUpdate, prevChannelUpdate, commitments) + val channels1 = channels + (channelId -> channel) + val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap + context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) + val scid2channels1 = scid2channels ++ mappings + val node2channels1 = node2channels.addOne(remoteNodeId, channelId) + apply(nodeParams, register, channels1, scid2channels1, node2channels1) - case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortChannelId, remoteNodeId)) => - context.log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId") - val node2channels1 = node2channels.subtractOne(remoteNodeId, shortChannelId) - apply(nodeParams, register, channelUpdates - shortChannelId, node2channels1) + case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortIds, remoteNodeId)) => + context.log.debug(s"removed local channel info for channelId=$channelId localAlias=${shortIds.localAlias}") + val channels1 = channels - channelId + val scid2Channels1 = scid2channels - shortIds.localAlias -- shortIds.real.toOption + val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId) + apply(nodeParams, register, channels1, scid2Channels1, node2channels1) - case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortChannelId, commitments)) => - val channelUpdates1 = channelUpdates.get(shortChannelId) match { + case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortIds, commitments)) => + val channels1 = channels.get(channelId) match { case Some(c: Relayer.OutgoingChannel) => - context.log.debug(s"available balance changed for channelId=$channelId shortChannelId=$shortChannelId availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) - channelUpdates + (shortChannelId -> c.copy(commitments = commitments)) - case None => channelUpdates // we only consider the balance if we have the channel_update + context.log.debug(s"available balance changed for channelId=$channelId localAlias=${shortIds.localAlias} availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) + channels + (channelId -> c.copy(commitments = commitments)) + case None => channels // we only consider the balance if we have the channel_update } - apply(nodeParams, register, channelUpdates1, node2channels) + apply(nodeParams, register, channels1, scid2channels, node2channels) - case WrappedShortChannelIdAssigned(ShortChannelIdAssigned(_, channelId, shortChannelId, previousShortChannelId_opt)) => - val (channelUpdates1, node2channels1) = previousShortChannelId_opt match { - case Some(previousShortChannelId) if previousShortChannelId != shortChannelId => - context.log.debug(s"shortChannelId changed for channelId=$channelId ($previousShortChannelId->$shortChannelId, probably due to chain re-org)") - // We simply remove the old entry: we should receive a LocalChannelUpdate with the new shortChannelId shortly. - val node2channels1 = channelUpdates.get(previousShortChannelId).map(_.nextNodeId) match { - case Some(remoteNodeId) => node2channels.subtractOne(remoteNodeId, previousShortChannelId) - case None => node2channels - } - (channelUpdates - previousShortChannelId, node2channels1) - case _ => (channelUpdates, node2channels) - } - apply(nodeParams, register, channelUpdates1, node2channels1) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index fc02f467eb..05abf32558 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -22,7 +22,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.{TypedActorContextOps, TypedActorRefOps} import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment.IncomingPaymentPacket.NodeRelayPacket @@ -106,7 +106,7 @@ object NodeRelay { val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward) if (upstream.amountIn - payloadOut.amountToForward < fee) { Some(TrampolineFeeInsufficient) - } else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.expiryDelta) { + } else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.channelConf.expiryDelta) { Some(TrampolineExpiryTooSoon) } else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) { Some(TrampolineExpiryTooSoon) @@ -163,7 +163,7 @@ class NodeRelay private(nodeParams: NodeParams, register: ActorRef, relayId: UUID, paymentHash: ByteVector32, - paymentSecret: ByteVector32, + paymentSecret: Option[ByteVector32], context: ActorContext[NodeRelay.Command], outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory) { @@ -267,18 +267,18 @@ class NodeRelay private(nodeParams: NodeParams, // If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient. val payFSM = payloadOut.invoiceFeatures match { case Some(features) => - val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil) + val extraEdges = payloadOut.invoiceRoutingInfo.getOrElse(Nil).flatMap(Bolt11Invoice.toExtraEdges(_, payloadOut.outgoingNodeId)) val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay if (Features(features).hasFeature(Features.BasicMultiPartPayment)) { context.log.debug("sending the payment to non-trampoline recipient using MPP") - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, routingHints, routeParams) + val payment = SendMultiPartPayment(payFsmAdapters, Some(paymentSecret), payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) payFSM ! payment payFSM } else { context.log.debug("sending the payment to non-trampoline recipient without MPP") - val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata) - val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams) + val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, Some(paymentSecret), payloadOut.paymentMetadata) + val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) payFSM ! payment payFSM @@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams, context.log.debug("sending the payment to the next trampoline node") val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) + val payment = SendMultiPartPayment(payFsmAdapters, Some(paymentSecret), payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) payFSM ! payment payFSM } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index cecd1cf5cb..71c6723396 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment.relay import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment._ import fr.acinq.eclair.{Logs, NodeParams} @@ -38,7 +38,7 @@ object NodeRelayer { // @formatter:off sealed trait Command case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket) extends Command - case class RelayComplete(childHandler: ActorRef[NodeRelay.Command], paymentHash: ByteVector32, paymentSecret: ByteVector32) extends Command + case class RelayComplete(childHandler: ActorRef[NodeRelay.Command], paymentHash: ByteVector32, paymentSecret: Option[ByteVector32]) extends Command private[relay] case class GetPendingPayments(replyTo: akka.actor.ActorRef) extends Command // @formatter:on @@ -48,7 +48,7 @@ object NodeRelayer { case _: GetPendingPayments => Logs.mdc() } - case class PaymentKey(paymentHash: ByteVector32, paymentSecret: ByteVector32) + case class PaymentKey(paymentHash: ByteVector32, paymentSecret: Option[ByteVector32]) /** * @param children a map of pending payments. We must index by both payment hash and payment secret because we may diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index e26e652db4..f40b4593b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.payment.relay import akka.Done import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ @@ -324,7 +324,7 @@ object PostRestartHtlcCleaner { } /** @return incoming HTLCs that have been *cross-signed* (that potentially have been relayed). */ - private def getIncomingHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey)(implicit log: LoggingAdapter): Seq[IncomingHtlc] = { + private def getIncomingHtlcs(channels: Seq[PersistentChannelData], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey)(implicit log: LoggingAdapter): Seq[IncomingHtlc] = { // We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed). // They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when // we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT. @@ -356,7 +356,7 @@ object PostRestartHtlcCleaner { .toMap /** @return pending outgoing HTLCs, grouped by their upstream origin. */ - private def getHtlcsRelayedOut(channels: Seq[HasCommitments], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = { + private def getHtlcsRelayedOut(channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = { val htlcsOut = channels .flatMap { c => // Filter out HTLCs that will never reach the blockchain or have already been timed-out on-chain. @@ -366,11 +366,13 @@ object PostRestartHtlcCleaner { val overriddenHtlcs: Set[Long] = (closingType_opt match { case Some(c: Closing.LocalClose) => Closing.overriddenOutgoingHtlcs(d, c.localCommitPublished.commitTx) case Some(c: Closing.RemoteClose) => Closing.overriddenOutgoingHtlcs(d, c.remoteCommitPublished.commitTx) + case Some(c: Closing.RevokedClose) => Closing.overriddenOutgoingHtlcs(d, c.revokedCommitPublished.commitTx) case _ => Set.empty[UpdateAddHtlc] }).map(_.id) val irrevocablySpent = closingType_opt match { case Some(c: Closing.LocalClose) => c.localCommitPublished.irrevocablySpent.values.toSeq case Some(c: Closing.RemoteClose) => c.remoteCommitPublished.irrevocablySpent.values.toSeq + case Some(c: Closing.RevokedClose) => c.revokedCommitPublished.irrevocablySpent.values.toSeq case _ => Nil } val timedOutHtlcs: Set[Long] = (closingType_opt match { @@ -396,7 +398,7 @@ object PostRestartHtlcCleaner { * and before it has effectively been removed. Such closed channels will automatically be removed once the channel is * restored. */ - private def listLocalChannels(channelsDb: ChannelsDb): Seq[HasCommitments] = + private def listLocalChannels(channelsDb: ChannelsDb): Seq[PersistentChannelData] = channelsDb.listLocalChannels().filterNot(c => Closing.isClosed(c, None).isDefined) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index c2af6d7f9d..c7341e7fab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -23,15 +23,17 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props, typed} import akka.event.Logging.MDC import akka.event.LoggingAdapter -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId} +import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams} import grizzled.slf4j.Logging import scala.concurrent.Promise +import scala.concurrent.duration.FiniteDuration /** * Created by PM on 01/02/2017. @@ -119,7 +121,8 @@ object Relayer extends Logging { case class RelayParams(publicChannelFees: RelayFees, privateChannelFees: RelayFees, - minTrampolineFees: RelayFees) { + minTrampolineFees: RelayFees, + enforcementDelay: FiniteDuration) { def defaultFees(announceChannel: Boolean): RelayFees = { if (announceChannel) { publicChannelFees @@ -130,7 +133,13 @@ object Relayer extends Logging { } case class RelayForward(add: UpdateAddHtlc) - case class UsableBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean) + case class ChannelBalance(remoteNodeId: PublicKey, shortIds: ShortIds, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) + + sealed trait OutgoingChannelParams { + def channelId: ByteVector32 + def channelUpdate: ChannelUpdate + def prevChannelUpdate: Option[ChannelUpdate] + } /** * Get the list of local outgoing channels. @@ -138,13 +147,15 @@ object Relayer extends Logging { * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: AbstractCommitments) { - def toUsableBalance: UsableBalance = UsableBalance( + case class OutgoingChannel(shortIds: ShortIds, nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { + override val channelId: ByteVector32 = commitments.channelId + def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, - shortChannelId = channelUpdate.shortChannelId, + shortIds = shortIds, canSend = commitments.availableBalanceForSend, canReceive = commitments.availableBalanceForReceive, - isPublic = commitments.announceChannel) + isPublic = commitments.announceChannel, + isEnabled = channelUpdate.channelFlags.isEnabled) } case class OutgoingChannels(channels: Seq[OutgoingChannel]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala index ebcdae283f..fdab6ee2a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.payment.send import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket -import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, PaymentRequest, RemoteFailure} +import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, Bolt11Invoice, Invoice, RemoteFailure} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TimestampSecond, randomBytes32, randomLong} @@ -53,19 +53,19 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case TickProbe => pickPaymentDestination(nodeParams.nodeId, routingData) match { case Some(targetNodeId) => - val fakeInvoice = PaymentRequest( - PaymentRequest.prefixes(nodeParams.chainHash), + val fakeInvoice = Bolt11Invoice( + Bolt11Invoice.prefixes(nodeParams.chainHash), Some(PAYMENT_AMOUNT_MSAT), TimestampSecond.now(), targetNodeId, List( - PaymentRequest.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!) - PaymentRequest.Description("ignored"), + Bolt11Invoice.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!) + Bolt11Invoice.Description("ignored"), ), ByteVector.empty) log.info(s"sending payment probe to node=$targetNodeId payment_hash=${fakeInvoice.paymentHash}") val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams - paymentInitiator ! PaymentInitiator.SendPaymentToNode(PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1, routeParams = routeParams) + paymentInitiator ! PaymentInitiator.SendPaymentToNode(self, PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1, routeParams = routeParams) case None => log.info(s"could not find a destination, re-scheduling") scheduleProbe() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index df978d2d56..42d0e93e33 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -18,13 +18,13 @@ package fr.acinq.eclair.payment.send import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} +import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -72,7 +72,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(RouteResponse(routes), d: PaymentProgress) => log.info("{} routes found (attempt={}/{})", routes.length, d.request.maxAttempts - d.remainingAttempts + 1, d.request.maxAttempts) // We may have already succeeded sending parts of the payment and only need to take care of the rest. - val (toSend, maxFee) = remainingToSend(d.request, d.pending.values) + val (toSend, maxFee) = remainingToSend(d.request, d.pending.values, d.request.routeParams.includeLocalChannelCost) if (routes.map(_.amount).sum == toSend) { val childPayments = routes.map(route => (UUID.randomUUID(), route)).toMap childPayments.foreach { case (childId, route) => spawnChildPaymentFsm(childId) ! createChildPayment(self, route, d.request) } @@ -92,7 +92,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, // Channels are mostly ignored for temporary reasons, likely because they didn't have enough balance to forward // the payment. When we're retrying an MPP split, it may make sense to retry those ignored channels because with // a different split, they may have enough balance to forward the payment. - val (toSend, maxFee) = remainingToSend(d.request, d.pending.values) + val (toSend, maxFee) = remainingToSend(d.request, d.pending.values, d.request.routeParams.includeLocalChannelCost) if (d.ignore.channels.nonEmpty) { log.debug("retry sending {} with maximum fee {} without ignoring channels ({})", toSend, maxFee, d.ignore.channels.map(_.shortChannelId).mkString(",")) val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry @@ -105,7 +105,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (cfg.storeInDb && d.pending.isEmpty && d.failures.isEmpty) { // In cases where we fail early (router error during the first attempt), the DB won't have an entry for that // payment, which may be confusing for users. - val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending) + val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending) nodeParams.db.payments.addOutgoingPayment(dummyPayment) nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil)) } @@ -117,8 +117,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) - val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) - stay() using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) + val extraEdges1 = PaymentFailure.updateExtraEdges(pf.failures, d.request.extraEdges) + stay() using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(extraEdges = extraEdges1)) } // The recipient released the preimage without receiving the full payment amount. @@ -139,12 +139,12 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) - val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) + val extraEdges1 = PaymentFailure.updateExtraEdges(pf.failures, d.request.extraEdges) val stillPending = d.pending - pf.id - val (toSend, maxFee) = remainingToSend(d.request, stillPending.values) + val (toSend, maxFee) = remainingToSend(d.request, stillPending.values, d.request.routeParams.includeLocalChannelCost) log.debug("child payment failed, retry sending {} with maximum fee {}", toSend, maxFee) val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry - val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) + val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(extraEdges = extraEdges1)) router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d1, cfg) goto(WAIT_FOR_ROUTES) using d1 } @@ -264,7 +264,11 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, } paymentSent.feesPaid + localFees } - context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId)) + val hints_opt = cfg.invoice.flatMap { + case invoice: Bolt11Invoice if invoice.routingInfo.nonEmpty => Some(invoice.routingInfo) + case _ => None + } + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId, hints_opt)) } Metrics.SentPaymentDuration .withTag(Tags.MultiPart, Tags.MultiPartType.Parent) @@ -309,19 +313,19 @@ object MultiPartPaymentLifecycle { * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). * @param maxAttempts maximum number of retries. * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). - * @param assistedRoutes routing hints (usually from a Bolt 11 invoice). + * @param extraEdges routing hints (usually from a Bolt 11 invoice). * @param routeParams parameters to fine-tune the routing algorithm. * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. */ case class SendMultiPartPayment(replyTo: ActorRef, - paymentSecret: ByteVector32, + paymentSecret: Option[ByteVector32], targetNodeId: PublicKey, totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, maxAttempts: Int, paymentMetadata: Option[ByteVector], - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil) { @@ -395,7 +399,7 @@ object MultiPartPaymentLifecycle { d.request.targetNodeId, toSend, maxFee, - d.request.assistedRoutes, + d.request.extraEdges, d.ignore, routeParams, allowMultiPart = true, @@ -416,10 +420,10 @@ object MultiPartPaymentLifecycle { case _ => false } - private def remainingToSend(request: SendMultiPartPayment, pending: Iterable[Route]): (MilliSatoshi, MilliSatoshi) = { + private def remainingToSend(request: SendMultiPartPayment, pending: Iterable[Route], includeLocalChannelCost: Boolean): (MilliSatoshi, MilliSatoshi) = { val sentAmount = pending.map(_.amount).sum - val sentFees = pending.map(_.fee).sum - (request.totalAmount - sentAmount, request.routeParams.copy(randomize = false).getMaxFee(request.totalAmount) - sentFees) + val sentFees = pending.map(_.fee(includeLocalChannelCost)).sum + (request.totalAmount - sentAmount, request.routeParams.getMaxFee(request.totalAmount) - sentFees) } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala new file mode 100644 index 0000000000..61992b06bb --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -0,0 +1,199 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment.send + +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import akka.actor.{ActorRef, typed} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} +import fr.acinq.eclair.message.Postman.{OnionMessageResponse, SendMessage} +import fr.acinq.eclair.message.{OnionMessages, Postman} +import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived +import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode +import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentFailed, PaymentSent} +import fr.acinq.eclair.router.Router.RouteParams +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} +import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.ReplyPath +import fr.acinq.eclair.wire.protocol.{OnionMessage, OnionMessagePayloadTlv} +import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32, randomKey} + +import java.util.UUID +import scala.util.Random + +object OfferPayment { + sealed trait Result + case class Success(invoice: Bolt12Invoice, payerKey: PrivateKey, paymentPreimage: ByteVector32) extends Result + case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends Result + case class UnsupportedChains(chains: Seq[ByteVector32]) extends Result + case class UnsupportedCurrency(iso4217: String) extends Result + case class ExpiredOffer(expiryDate: TimestampSecond) extends Result + case class QuantityTooLow(quantityMin: Long) extends Result + case class QuantityTooHigh(quantityMax: Long) extends Result + case object IsSendInvoice extends Result + case class AmountInsufficient(amountNeeded: MilliSatoshi) extends Result + case class InvalidSignature(signature: ByteVector64) extends Result + case class InvalidInvoice(invoice: Bolt12Invoice, request: InvoiceRequest, payerKey: PrivateKey) extends Result + case object NoInvoice extends Result + case class FailedPayment(invoice: Bolt12Invoice, payerKey: PrivateKey, paymentFailed: PaymentFailed) extends Result + + sealed trait Command + case class PayOffer(replyTo: typed.ActorRef[Result]) extends Command + case class WrappedMessageResponse(response: OnionMessageResponse) extends Command + case class WrappedPaymentId(paymentId: UUID) extends Command + case class WrappedPreimageReceived(preimageReceived: PreimageReceived) extends Command + case class WrappedPaymentSent(paymentSent: PaymentSent) extends Command + case class WrappedPaymentFailed(paymentFailed: PaymentFailed) extends Command + + case class SendPaymentConfig(externalId_opt: Option[String], + maxAttempts: Int, + routeParams: RouteParams) + + def paymentResultWrapper(x: Any): Command = { + x match { + case paymentId: UUID => WrappedPaymentId(paymentId) + case preimageReceived: PreimageReceived => WrappedPreimageReceived(preimageReceived) + case paymentSent: PaymentSent => WrappedPaymentSent(paymentSent) + case paymentFailed: PaymentFailed => WrappedPaymentFailed(paymentFailed) + } + } + + def buildRequest(nodeParams: NodeParams, request: InvoiceRequest, destination: OnionMessages.Destination): (ByteVector32, PublicKey, OnionMessage) = { + val pathId = randomBytes32() + // TODO: randomize intermediate nodes + val intermediateNodes = Seq.empty + val replyPath = ReplyPath(OnionMessages.buildRoute(randomKey(), intermediateNodes.reverse, OnionMessages.Recipient(nodeParams.nodeId, Some(pathId.bytes)))) + val (nextNodeId, message) = OnionMessages.buildMessage(randomKey(), randomKey(), intermediateNodes, destination, Seq(replyPath, OnionMessagePayloadTlv.InvoiceRequest(request))) + (pathId, nextNodeId, message) + } + + def apply(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + offer: Offer, + amount: MilliSatoshi, + quantity: Long, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + Behaviors.receivePartial { + case (context, PayOffer(replyTo)) => + if (!nodeParams.features.invoiceFeatures().areSupported(offer.features)) { + replyTo ! UnsupportedFeatures(offer.features) + Behaviors.stopped + } else if (!offer.chains.contains(nodeParams.chainHash)) { + replyTo ! UnsupportedChains(offer.chains) + Behaviors.stopped + } else if (offer.currency.nonEmpty) { + replyTo ! UnsupportedCurrency(offer.currency.get) + Behaviors.stopped + } else if (offer.expiry.exists(_ < TimestampSecond.now())) { + replyTo ! ExpiredOffer(offer.expiry.get) + Behaviors.stopped + } else if (offer.quantityMin.exists(_ > quantity)) { + replyTo ! QuantityTooLow(offer.quantityMin.get) + Behaviors.stopped + } else if (offer.quantityMax.getOrElse(1L) < quantity) { + replyTo ! QuantityTooHigh(offer.quantityMax.getOrElse(1)) + Behaviors.stopped + } else if (offer.sendInvoice) { + replyTo ! IsSendInvoice + Behaviors.stopped + } else if (offer.amount.exists(_ * quantity > amount)) { + replyTo ! AmountInsufficient(offer.amount.get * quantity) + Behaviors.stopped + } else if (offer.signature.nonEmpty && !offer.checkSignature()) { + replyTo ! InvalidSignature(offer.signature.get) + Behaviors.stopped + } else { + val payerKey = randomKey() + val request = InvoiceRequest(offer, amount, quantity, nodeParams.features.invoiceFeatures(), payerKey, nodeParams.chainHash) + sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, offer, request, payerKey, replyTo, nodeParams.onionMessageConfig.maxAttempts, sendPaymentConfig) + } + } + } + + val rand = new Random + + def sendInvoiceRequest(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + context: ActorContext[Command], + offer: Offer, + request: InvoiceRequest, + payerKey: PrivateKey, + replyTo: typed.ActorRef[Result], + remainingAttempts: Int, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + val destination = offer.contact match { + case Left(blindedRoutes) => + val blindedRoute = blindedRoutes(rand.nextInt(blindedRoutes.length)) + OnionMessages.BlindedPath(blindedRoute) + case Right(nodeId) => + OnionMessages.Recipient(nodeId, None, None) + } + val (pathId, nextNodeId, message) = buildRequest(nodeParams, request, destination) + postman ! SendMessage(nextNodeId, message, Some(pathId), context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) + waitForInvoice(nodeParams, postman, paymentInitiator, offer, request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) + } + + def waitForInvoice(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + offer: Offer, + request: InvoiceRequest, + payerKey: PrivateKey, + replyTo: typed.ActorRef[Result], + remainingAttempts: Int, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + Behaviors.receivePartial { + case (context, WrappedMessageResponse(Postman.Response(payload, pathId))) if payload.invoice.nonEmpty => + val invoice = payload.invoice.get.invoice + if (invoice.isValidFor(offer, request)) { + val recipientAmount = invoice.amount + paymentInitiator ! SendPaymentToNode(context.messageAdapter(paymentResultWrapper).toClassic, recipientAmount, invoice, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams) + waitForPayment(invoice, payerKey, replyTo) + } else { + replyTo ! InvalidInvoice(invoice, request, payerKey) + Behaviors.stopped + } + case (context, WrappedMessageResponse(_)) if remainingAttempts > 0 => + // We didn't get an invoice, let's retry. + sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, offer, request, payerKey, replyTo, remainingAttempts, sendPaymentConfig) + case (_, WrappedMessageResponse(_)) => + replyTo ! NoInvoice + Behaviors.stopped + } + } + + def waitForPayment(invoice: Bolt12Invoice, payerKey: PrivateKey, replyTo: typed.ActorRef[Result]): Behavior[Command] = { + Behaviors.receiveMessagePartial { + case WrappedPaymentId(_) => + Behaviors.same + case WrappedPreimageReceived(preimageReceived) => + replyTo ! Success(invoice, payerKey, preimageReceived.paymentPreimage) + Behaviors.stopped + case WrappedPaymentSent(paymentSent) => + replyTo ! Success(invoice, payerKey, paymentSent.paymentPreimage) + Behaviors.stopped + case WrappedPaymentFailed(failed) => + replyTo ! FailedPayment(invoice, payerKey, failed) + Behaviors.stopped + } + } +} + + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala index 921a8c5bb5..8ac3ed8bb8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment.send -import fr.acinq.eclair.Features +import fr.acinq.eclair.{Features, InvoiceFeature} sealed trait PaymentError extends Throwable @@ -25,9 +25,7 @@ object PaymentError { // @formatter:off sealed trait InvalidInvoice extends PaymentError /** The invoice contains a feature we don't support. */ - case class UnsupportedFeatures(features: Features) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } - /** The invoice is missing a payment secret. */ - case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" } + case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } // @formatter:on // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 5b39035302..5ae91771a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -17,20 +17,20 @@ package fr.acinq.eclair.payment.send import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, Props} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.Features.BasicMultiPartPayment -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32} import java.util.UUID import scala.util.{Failure, Success, Try} @@ -46,27 +46,25 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn def main(pending: Map[UUID, PendingPayment]): Receive = { case r: SendPaymentToNode => + val replyTo = if (r.replyTo == ActorRef.noSender) sender() else r.replyTo val paymentId = UUID.randomUUID() if (!r.blockUntilComplete) { // Immediately return the paymentId - sender() ! paymentId + replyTo ! paymentId } - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) - r.paymentRequest.paymentSecret match { - case _ if !r.paymentRequest.features.areSupported(nodeParams) => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.paymentRequest.features.features)) :: Nil) - case None => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) - case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) => - val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) - case Some(paymentSecret) => - val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs) - val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) + if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) { + replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) + } else if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment)) { + val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) + fsm ! SendMultiPartPayment(self, r.invoice.paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) + } else { + val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, r.invoice.paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) + val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) + fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.extraEdges, r.routeParams) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) } case r: SendSpontaneousPayment => @@ -85,7 +83,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn r.trampolineAttempts match { case Nil => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineFeesMissing) :: Nil) - case _ if !r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty => + case _ if !r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype) && r.invoice.amount_opt.isEmpty => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineLegacyAmountLessInvoice) :: Nil) case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts => log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") @@ -103,10 +101,8 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { - case _ if r.paymentRequest.paymentSecret.isEmpty => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta) match { @@ -115,7 +111,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.paymentRequest.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, Some(trampolineSecret), r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -124,7 +120,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata), r.paymentRequest.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret, r.invoice.paymentMetadata), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) @@ -133,7 +129,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case pf: PaymentFailed => pending.get(pf.id).foreach { case pp: PendingTrampolinePayment => val trampolineRoute = Seq( - NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.expiryDelta, 0 msat), + NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1) ) val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f } @@ -194,19 +190,22 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn private def buildTrampolinePayment(r: SendRequestedPayment, trampolineNodeId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[(MilliSatoshi, CltvExpiry, OnionRoutingPacket)] = { val trampolineRoute = Seq( - NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.expiryDelta, 0 msat), + NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop ) - val finalPayload = if (r.paymentRequest.features.allowMultiPart) { - PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata) + val finalPayload = if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment)) { + PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret, r.invoice.paymentMetadata) } else { - PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata) + PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret, r.invoice.paymentMetadata) } // We assume that the trampoline node supports multi-part payments (it should). - val trampolinePacket_opt = if (r.paymentRequest.features.allowTrampoline) { + val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, finalPayload) } else { - OutgoingPaymentPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload) + r.invoice match { + case invoice: Bolt11Invoice => OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, finalPayload) + case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) + } } trampolinePacket_opt.map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) @@ -214,13 +213,13 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[Unit] = { - val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees))) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees))) // We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node. val trampolineSecret = randomBytes32() buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.paymentMetadata, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + fsm ! SendMultiPartPayment(self, Some(trampolineSecret), r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } } @@ -269,12 +268,11 @@ object PaymentInitiator { sealed trait SendRequestedPayment { // @formatter:off def recipientAmount: MilliSatoshi - def paymentRequest: PaymentRequest - def recipientNodeId: PublicKey = paymentRequest.nodeId - def paymentHash: ByteVector32 = paymentRequest.paymentHash - def fallbackFinalExpiryDelta: CltvExpiryDelta + def invoice: Invoice + def recipientNodeId: PublicKey = invoice.nodeId + def paymentHash: ByteVector32 = invoice.paymentHash // We add one block in order to not have our htlcs fail when a new block has just been found. - def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = paymentRequest.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1) + def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.toCltvExpiry(currentBlockHeight + 1) // @formatter:on } @@ -283,40 +281,37 @@ object PaymentInitiator { * Once we have trampoline fee estimation built into the router, the decision to use Trampoline or not should be done * automatically by the router instead of the caller. * - * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param paymentRequest Bolt 11 invoice. - * @param trampolineNodeId id of the trampoline node. - * @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries, - * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. - * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 - * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. - * @param routeParams (optional) parameters to fine-tune the routing algorithm. + * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * @param invoice Bolt 11 invoice. + * @param trampolineNodeId id of the trampoline node. + * @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries, + * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. + * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 + * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. + * @param routeParams (optional) parameters to fine-tune the routing algorithm. */ case class SendTrampolinePayment(recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, + invoice: Invoice, trampolineNodeId: PublicKey, trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, routeParams: RouteParams) extends SendRequestedPayment /** * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param paymentRequest Bolt 11 invoice. + * @param invoice invoice. * @param maxAttempts maximum number of retries. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). - * @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice). + * @param extraEdges (optional) routing hints (usually from a Bolt 11 invoice). * @param routeParams (optional) parameters to fine-tune the routing algorithm. * @param userCustomTlvs (optional) user-defined custom tlvs that will be added to the onion sent to the target node. * @param blockUntilComplete (optional) if true, wait until the payment completes before returning a result. */ - case class SendPaymentToNode(recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, + case class SendPaymentToNode(replyTo: ActorRef, + recipientAmount: MilliSatoshi, + invoice: Invoice, maxAttempts: Int, - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, externalId: Option[String] = None, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams, userCustomTlvs: Seq[GenericTlv] = Nil, blockUntilComplete: Boolean = false) extends SendRequestedPayment @@ -355,31 +350,29 @@ object PaymentInitiator { * SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) * SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) * - * @param amount amount that should be received by the last node in the route (should take trampoline - * fees into account). - * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * This amount may be split between multiple requests if using MPP. - * @param paymentRequest Bolt 11 invoice. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. - * @param route route to use to reach either the final recipient or the first trampoline node. - * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). - * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make - * sure all partial payments use the same parentId. If not provided, a random parentId will - * be generated that can be used for the remaining partial payments. - * @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline - * node against probing. When manually sending a multi-part payment, you need to make sure - * all partial payments use the same trampolineSecret. - * @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same - * for all partial payments in the set. - * @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be - * the same for all partial payments in the set. - * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a - * single trampoline node). + * @param amount amount that should be received by the last node in the route (should take trampoline + * fees into account). + * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * This amount may be split between multiple requests if using MPP. + * @param invoice Bolt 11 invoice. + * @param route route to use to reach either the final recipient or the first trampoline node. + * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). + * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make + * sure all partial payments use the same parentId. If not provided, a random parentId will + * be generated that can be used for the remaining partial payments. + * @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline + * node against probing. When manually sending a multi-part payment, you need to make sure + * all partial payments use the same trampolineSecret. + * @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same + * for all partial payments in the set. + * @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be + * the same for all partial payments in the set. + * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a + * single trampoline node). */ case class SendPaymentToRoute(amount: MilliSatoshi, recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, + invoice: Invoice, route: PredefinedRoute, externalId: Option[String], parentId: Option[UUID], @@ -409,7 +402,7 @@ object PaymentInitiator { * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). * @param recipientNodeId id of the final recipient. * @param upstream information about the payment origin (to link upstream to downstream when relaying a payment). - * @param paymentRequest Bolt 11 invoice. + * @param invoice Bolt 11 invoice. * @param storeInDb whether to store data in the payments DB (e.g. when we're relaying a trampoline payment, we * don't want to store in the DB). * @param publishEvent whether to publish a [[fr.acinq.eclair.payment.PaymentEvent]] on success/failure (e.g. for @@ -425,7 +418,7 @@ object PaymentInitiator { recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, upstream: Upstream, - paymentRequest: Option[PaymentRequest], + invoice: Option[Invoice], storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, recordPathFindingMetrics: Boolean, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 734ed5b8a5..e169af20c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -16,17 +16,18 @@ package fr.acinq.eclair.payment.send +import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} +import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -56,19 +57,19 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(c: SendPaymentToRoute, WaitingForRequest) => log.debug("sending {} to route {}", c.finalPayload.amount, c.printRoute()) c.route.fold( - hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.assistedRoutes, paymentContext = Some(cfg.paymentContext)), + hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.extraEdges, paymentContext = Some(cfg.paymentContext)), route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) case Event(c: SendPaymentToNode, WaitingForRequest) => log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } @@ -78,7 +79,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") OutgoingPaymentPacket.buildCommand(self, cfg.upstream, paymentHash, route.hops, c.finalPayload) match { case Success((cmd, sharedSecrets)) => - register ! Register.ForwardShortId(self, route.hops.head.lastUpdate.shortChannelId, cmd) + register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], route.hops.head.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) case Failure(t) => log.warning("cannot send outgoing payment: {}", t.getMessage) @@ -102,6 +103,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A handleLocalFail(d, DisconnectedException, isFatal = false) case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) => + router ! Router.RouteDidRelay(d.route) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1) val p = PartialPayment(id, d.c.finalPayload.amount, d.cmd.amount - d.c.finalPayload.amount, htlc.channelId, Some(cfg.fullRoute(d.route))) myStop(d.c, Right(cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil))) @@ -137,7 +139,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A data.c match { case sendPaymentToNode: SendPaymentToNode => val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore) - router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, sendPaymentToNode.maxFee, data.c.assistedRoutes, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.c, data.failures :+ failure, ignore1) case _: SendPaymentToRoute => log.error("unexpected retry during SendPaymentToRoute") @@ -166,13 +168,31 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = { import d._ - (Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { case success@Success(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.finalPayload.amount, Nil, e))).increment() success case failure@Failure(_) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil))).increment() failure + }) match { + case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => + // We have discovered some liquidity information with this payment: we update the router accordingly. + val stoppedRoute = d.route.stopAt(nodeId) + if (stoppedRoute.hops.length > 1) { + router ! Router.RouteCouldRelay(stoppedRoute) + } + failureMessage match { + case TemporaryChannelFailure(update) => + d.route.hops.find(_.nodeId == nodeId) match { + case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.FromAnnouncement(update), true) => + router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop) + case _ => // otherwise the relay parameters may have changed, so it's not necessarily a liquidity issue + } + case _ => // other errors should not be used for liquidity issues + } + res + case res => res }) match { case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately @@ -206,7 +226,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) if (Announcements.checkSig(failureMessage.update, nodeId)) { - val assistedRoutes1 = handleUpdate(nodeId, failureMessage, d) + val extraEdges1 = handleUpdate(nodeId, failureMessage, d) val ignore1 = PaymentFailure.updateIgnored(failure, ignore) // let's try again, router will have updated its state c match { @@ -214,7 +234,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, assistedRoutes1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore1) } } else { @@ -225,7 +245,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore + nodeId) } } @@ -241,7 +261,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A * * @return updated routing hints if applicable. */ - private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[Seq[ExtraHop]] = { + private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[ExtraEdge] = { + // TODO: properly handle updates to channels provided as routing hints in the invoice data.route.getChannelUpdateForNode(nodeId) match { case Some(u) if u.shortChannelId != failure.update.shortChannelId => // it is possible that nodes in the route prefer using a different channel (to the same N+1 node) than the one we requested, that's fine @@ -266,21 +287,17 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! failure.update // we return updated assisted routes: they take precedence over the router's routing table if (failure.update.channelFlags.isEnabled) { - data.c.assistedRoutes.map(_.map { - case extraHop: ExtraHop if extraHop.shortChannelId == failure.update.shortChannelId => extraHop.copy( - cltvExpiryDelta = failure.update.cltvExpiryDelta, - feeBase = failure.update.feeBaseMsat, - feeProportionalMillionths = failure.update.feeProportionalMillionths - ) - case extraHop => extraHop - }) + data.c.extraEdges.map { + case edge: BasicEdge if edge.shortChannelId == failure.update.shortChannelId => edge.update(failure.update) + case edge => edge + } } else { // if the channel is disabled, we temporarily exclude it: this is necessary because the routing hint doesn't contain // channel flags to indicate that it's disabled - data.c.assistedRoutes.flatMap(r => RouteCalculation.toChannelDescs(r, data.c.targetNodeId)) - .find(_.shortChannelId == failure.update.shortChannelId) - .foreach(desc => router ! ExcludeChannel(desc)) // we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty - data.c.assistedRoutes + data.c.extraEdges + .find { case edge: BasicEdge => edge.shortChannelId == failure.update.shortChannelId } + .foreach { case edge: BasicEdge => router ! ExcludeChannel(ChannelDesc(edge.shortChannelId, edge.sourceNodeId, edge.targetNodeId)) } // we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty + data.c.extraEdges } } @@ -333,7 +350,11 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } request match { case SendPaymentToNode(_, _, _, _, _, routeParams) => - context.system.eventStream.publish(PathFindingExperimentMetrics(request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, routeParams.experimentName, cfg.recipientNodeId)) + val hints_opt = cfg.invoice.flatMap { + case invoice: Bolt11Invoice if invoice.routingInfo.nonEmpty => Some(invoice.routingInfo) + case _ => None + } + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, routeParams.experimentName, cfg.recipientNodeId, hints_opt)) case SendPaymentToRoute(_, _, _, _) => () } } @@ -359,7 +380,7 @@ object PaymentLifecycle { // @formatter:off def replyTo: ActorRef def finalPayload: FinalPayload - def assistedRoutes: Seq[Seq[ExtraHop]] + def extraEdges: Seq[ExtraEdge] def targetNodeId: PublicKey def maxAttempts: Int // @formatter:on @@ -374,7 +395,7 @@ object PaymentLifecycle { case class SendPaymentToRoute(replyTo: ActorRef, route: Either[PredefinedRoute, Route], finalPayload: FinalPayload, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil) extends SendPayment { + extraEdges: Seq[ExtraEdge] = Nil) extends SendPayment { require(route.fold(!_.isEmpty, _.hops.nonEmpty), "payment route must not be empty") val targetNodeId: PublicKey = route.fold(_.targetNodeId, _.hops.last.nextNodeId) @@ -391,18 +412,18 @@ object PaymentLifecycle { /** * Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route. * - * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline - * node when using trampoline). - * @param finalPayload onion payload for the target node. - * @param maxAttempts maximum number of retries. - * @param assistedRoutes routing hints (usually from a Bolt 11 invoice). - * @param routeParams parameters to fine-tune the routing algorithm. + * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline + * node when using trampoline). + * @param finalPayload onion payload for the target node. + * @param maxAttempts maximum number of retries. + * @param extraEdges routing hints (usually from a Bolt 11 invoice). + * @param routeParams parameters to fine-tune the routing algorithm. */ case class SendPaymentToNode(replyTo: ActorRef, targetNodeId: PublicKey, finalPayload: FinalPayload, maxAttempts: Int, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams) extends SendPayment { require(finalPayload.amount > 0.msat, s"amount must be > 0") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 8bea5c32de..d35bb6c37f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -18,24 +18,23 @@ package fr.acinq.eclair.remote import akka.actor.{ActorRef, ExtendedActorSystem} import akka.serialization.Serialization -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.io.Switchboard.RouterPeerConf import fr.acinq.eclair.io.{ClientSpawner, Peer, PeerConnection, Switchboard} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} -import fr.acinq.eclair.router.Router.{GossipDecision, MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries, SendChannelQuery} +import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features} import scodec._ import scodec.codecs._ -import java.net.{InetAddress, InetSocketAddress} import scala.concurrent.duration._ class EclairInternalsSerializer(val system: ExtendedActorSystem) extends ScodecSerializer(43, EclairInternalsSerializer.codec(system)) @@ -46,7 +45,7 @@ object EclairInternalsSerializer { def finiteDurationCodec: Codec[FiniteDuration] = int64.xmap(_.milliseconds, _.toMillis) - def iterable[A](codec: Codec[A]): Codec[Iterable[A]] = listOfN(uint16, codec).xmap(_.toIterable, _.toList) + def iterable[A](codec: Codec[A]): Codec[Iterable[A]] = listOfN(uint16, codec).xmap(_.toList, _.toList) val searchBoundariesCodec: Codec[SearchBoundaries] = ( ("maxFee" | millisatoshi) :: @@ -68,7 +67,8 @@ object EclairInternalsSerializer { val heuristicsConstantsCodec: Codec[HeuristicsConstants] = ( ("lockedFundsRisk" | double) :: ("failureCost" | relayFeesCodec) :: - ("hopCost" | relayFeesCodec)).as[HeuristicsConstants] + ("hopCost" | relayFeesCodec) :: + ("useLogProbability" | bool(8))).as[HeuristicsConstants] val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: @@ -87,7 +87,8 @@ object EclairInternalsSerializer { ).as[PathFindingExperimentConf] val routerConfCodec: Codec[RouterConf] = ( - ("channelExcludeDuration" | finiteDurationCodec) :: + ("watchSpentWindow" | finiteDurationCodec) :: + ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: ("requestNodeAnnouncements" | bool(8)) :: ("encodingType" | discriminated[EncodingType].by(uint8) @@ -95,9 +96,10 @@ object EclairInternalsSerializer { .typecase(1, provide(EncodingType.COMPRESSED_ZLIB))) :: ("channelRangeChunkSize" | int32) :: ("channelQueryChunkSize" | int32) :: - ("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf] + ("pathFindingExperimentConf" | pathFindingExperimentConfCodec) :: + ("balanceEstimateHalfLife" | finiteDurationCodec)).as[RouterConf] - val overrideFeaturesListCodec: Codec[List[(PublicKey, Features)]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) + val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) val peerConnectionConfCodec: Codec[PeerConnection.Conf] = ( ("authTimeout" | finiteDurationCodec) :: @@ -107,7 +109,8 @@ object EclairInternalsSerializer { ("pingDisconnect" | bool(8)) :: ("maxRebroadcastDelay" | finiteDurationCodec) :: ("killIdleDelay" | finiteDurationCodec) :: - ("maxOnionMessagesPerSecond" | int32)).as[PeerConnection.Conf] + ("maxOnionMessagesPerSecond" | int32) :: + ("sendRemoteAddressInit" | bool(8))).as[PeerConnection.Conf] val peerConnectionDoSyncCodec: Codec[PeerConnection.DoSync] = bool(8).as[PeerConnection.DoSync] @@ -130,28 +133,22 @@ object EclairInternalsSerializer { (path: String) => system.provider.resolveActorRef(path), (actor: ActorRef) => Serialization.serializedActorPath(actor)) - val inetAddressCodec: Codec[InetAddress] = discriminated[InetAddress].by(uint8) - .typecase(0, ipv4address) - .typecase(1, ipv6address) - - val inetSocketAddressCodec: Codec[InetSocketAddress] = (inetAddressCodec ~ uint16).xmap({ case (addr, port) => new InetSocketAddress(addr, port) }, socketAddr => (socketAddr.getAddress, socketAddr.getPort)) - def connectionRequestCodec(system: ExtendedActorSystem): Codec[ClientSpawner.ConnectionRequest] = ( - ("address" | inetSocketAddressCodec) :: - ("remoteNodeId" | publicKey) :: + ("remoteNodeId" | publicKey) :: + ("address" | nodeaddress) :: ("origin" | actorRefCodec(system)) :: ("isPersistent" | bool8)).as[ClientSpawner.ConnectionRequest] def initializeConnectionCodec(system: ExtendedActorSystem): Codec[PeerConnection.InitializeConnection] = ( ("peer" | actorRefCodec(system)) :: ("chainHash" | bytes32) :: - ("features" | variableSizeBytes(uint16, featuresCodec)) :: + ("features" | variableSizeBytes(uint16, initFeaturesCodec)) :: ("doSync" | bool(8))).as[PeerConnection.InitializeConnection] def connectionReadyCodec(system: ExtendedActorSystem): Codec[PeerConnection.ConnectionReady] = ( ("peerConnection" | actorRefCodec(system)) :: ("remoteNodeId" | publicKey) :: - ("address" | inetSocketAddressCodec) :: + ("address" | nodeaddress) :: ("outgoing" | bool(8)) :: ("localInit" | lengthPrefixedInitCodec) :: ("remoteInit" | lengthPrefixedInitCodec)).as[PeerConnection.ConnectionReady] @@ -183,7 +180,7 @@ object EclairInternalsSerializer { .typecase(11, initializeConnectionCodec(system)) .typecase(12, connectionReadyCodec(system)) .typecase(13, provide(PeerConnection.ConnectionResult.NoAddressFound)) - .typecase(14, inetSocketAddressCodec.as[PeerConnection.ConnectionResult.ConnectionFailed]) + .typecase(14, nodeaddress.as[PeerConnection.ConnectionResult.ConnectionFailed]) .typecase(15, variableSizeBytes(uint16, utf8).as[PeerConnection.ConnectionResult.AuthenticationFailed]) .typecase(16, variableSizeBytes(uint16, utf8).as[PeerConnection.ConnectionResult.InitializationFailed]) .typecase(17, (actorRefCodec(system) :: actorRefCodec(system)).as[PeerConnection.ConnectionResult.AlreadyConnected]) @@ -197,7 +194,7 @@ object EclairInternalsSerializer { .typecase(31, lengthPrefixedNodeAnnouncementCodec.as[NodeUpdated]) .typecase(32, publicKey.as[NodeLost]) .typecase(33, iterable(singleChannelDiscoveredCodec).as[ChannelsDiscovered]) - .typecase(34, shortchannelid.as[ChannelLost]) + .typecase(34, realshortchannelid.as[ChannelLost]) .typecase(35, iterable(lengthPrefixedChannelUpdateCodec).as[ChannelUpdatesReceived]) .typecase(36, double.as[SyncProgress]) .typecase(40, lengthPrefixedAnnouncementCodec.as[GossipDecision.Accepted]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index d07bd7ccc9..5441a3407f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -16,10 +16,10 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -28,16 +28,16 @@ import shapeless.HNil */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) - def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = + def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[Feature], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil)))) def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) - def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features): ByteVector = + def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = if (isNode1(localNodeId, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) } else { @@ -46,7 +46,7 @@ object Announcements { def signChannelAnnouncement(witness: ByteVector, key: PrivateKey): ByteVector64 = Crypto.sign(witness, key) - def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { + def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { val (nodeId1, nodeId2, bitcoinKey1, bitcoinKey2, nodeSignature1, nodeSignature2, bitcoinSignature1, bitcoinSignature2) = if (isNode1(localNodeId, remoteNodeId)) { (localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, localNodeSignature, remoteNodeSignature, localBitcoinSignature, remoteBitcoinSignature) @@ -68,7 +68,7 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { require(alias.length <= 32) val sortedAddresses = nodeAddresses.map { case address@(_: IPv4) => (1, address) @@ -76,8 +76,7 @@ object Announcements { case address@(_: Tor2) => (3, address) case address@(_: Tor3) => (4, address) }.sortBy(_._1).map(_._2) - val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures() - val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty) + val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( signature = sig, @@ -85,7 +84,7 @@ object Announcements { nodeId = nodeSecret.publicKey, rgbColor = color, alias = alias, - features = nodeAnnouncementFeatures, + features = features.unscoped(), addresses = sortedAddresses ) } @@ -118,7 +117,6 @@ object Announcements { def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = { val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) - val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, channelFlags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsatOpt, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) ChannelUpdate( @@ -135,6 +133,14 @@ object Announcements { ) } + def updateScid(nodeSecret: PrivateKey, u: ChannelUpdate, scid: ShortChannelId): ChannelUpdate = { + // NB: we don't update the timestamp as we're not changing any parameter. + val u1 = u.copy(shortChannelId = scid) + val witness = channelUpdateWitnessEncode(u.chainHash, scid, u.timestamp, u.channelFlags, u.cltvExpiryDelta, u.htlcMinimumMsat, u.feeBaseMsat, u.feeProportionalMillionths, u.htlcMaximumMsat, u.tlvStream) + val sig = Crypto.sign(witness, nodeSecret) + u1.copy(signature = sig) + } + def checkSigs(ann: ChannelAnnouncement): Boolean = { val witness = channelAnnouncementWitnessEncode(ann.chainHash, ann.shortChannelId, ann.nodeId1, ann.nodeId2, ann.bitcoinKey1, ann.bitcoinKey2, ann.features, ann.tlvStream) verifySignature(witness, ann.nodeSignature1, ann.nodeId1) && diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala new file mode 100644 index 0000000000..18ea2a810d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala @@ -0,0 +1,331 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.router + +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} +import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Route} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} + +/** + * Estimates the balance between a pair of nodes + * + * @param low lower bound on the balance + * @param lowTimestamp time at which the lower bound was known to be correct + * @param high upper bound on the balance + * @param highTimestamp time at which the upper bound was known to be correct + * @param capacities capacities of the channels between these two nodes + * @param halfLife time after which the certainty of the lower/upper bounds is halved + */ +case class BalanceEstimate private(low: MilliSatoshi, + lowTimestamp: TimestampSecond, + high: MilliSatoshi, highTimestamp: TimestampSecond, + capacities: Map[ShortChannelId, Satoshi], + halfLife: FiniteDuration) { + val maxCapacity: Satoshi = capacities.values.maxOption.getOrElse(0 sat) + + /* The goal of this class is to estimate the probability that a given edge can relay the amount that we plan to send + * through it. We model this probability with 3 pieces of linear functions. + * + * Without any information we use the following baseline (x is the amount we're sending and y the probability it can be relayed): + * + * 1 |**** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * 0 +------------------------------------------------**** + * 0 capacity + * + * If we get the information that the edge can (or can't) relay a given amount (because we tried), then we get a lower + * bound (or upper bound) that we can use and our model becomes: + * + * 1 |*************** + * | |* + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * 0 +--------------|-----------|************************* + * 0 low high capacity + * + * However this lower bound (or upper bound) is only valid at the moment we got that information. If we wait, the + * information decays and we slowly go back towards our baseline: + * + * 1 |***** + * | ***** + * | ***** + * | |** + * | | * + * | | ** + * | | ** + * | | * + * | | ** + * | | * + * | | ******** + * | | | ********* + * 0 +--------------|-----------|----------------********* + * 0 low high capacity + */ + + /** + * We model the decay with a half-life H: every H units of time, our confidence decreases by half and our estimated + * probability distribution gets closer to the baseline uniform distribution of balances between 0 and totalCapacity. + * + * @param amount the amount that we knew we could send or not send at time t + * @param successProbabilityAtT probability that we could relay amount at time t (usually 0 or 1) + * @param t time at which we knew if we could or couldn't send amount + * @return the probability that we can send amount now + */ + private def decay(amount: MilliSatoshi, successProbabilityAtT: Double, t: TimestampSecond): Double = { + val decayRatio = 1 / math.pow(2, (TimestampSecond.now() - t) / halfLife) + val baseline = 1 - amount.toLong.toDouble / maxCapacity.toMilliSatoshi.toLong + baseline * (1 - decayRatio) + successProbabilityAtT * decayRatio + } + + private def otherSide: BalanceEstimate = + BalanceEstimate(maxCapacity - high, highTimestamp, maxCapacity - low, lowTimestamp, capacities, halfLife) + + /** + * We tried to send the given amount and received a temporary channel failure. We assume that this failure was caused + * by a lack of liquidity: it could also be caused by a violation of max_accepted_htlcs, max_htlc_value_in_flight_msat + * or a spamming protection heuristic by the relaying node, but since we have no way of detecting that, our best + * strategy is to ignore these cases. + */ + def couldNotSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = { + if (amount <= low) { + // the balance is actually below `low`, we discard our previous lower bound + copy(low = 0 msat, lowTimestamp = timestamp, high = amount, highTimestamp = timestamp) + } else if (amount < high) { + // the balance is between `low` and `high` as we expected, we discard our previous upper bound + copy(high = amount, highTimestamp = timestamp) + } else { + // We already expected not to be able to relay that amount as it is above our upper bound. However if the upper bound + // was old enough that replacing it with the current amount decreases the success probability for `high`, then we + // replace it. + val updated = copy(high = amount, highTimestamp = timestamp) + if (updated.canSend(high) < this.canSend(high)) { + updated + } else { + this + } + } + } + + /** + * We tried to send the given amount, it was correctly relayed but failed afterwards, so we know we should be able to + * send at least this amount again. + */ + def couldSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = + otherSide.couldNotSend(maxCapacity - amount, timestamp).otherSide + + /** + * We successfully sent the given amount, so we know that some of the liquidity has shifted. + */ + def didSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = { + val newLow = (low - amount).max(0 msat) + if (capacities.size == 1) { + // Special case for single channel as we expect this case to be quite common and we can easily get more precise bounds. + val newHigh = (high - amount).max(0 msat) + // We could shift everything left by amount without changing the timestamps but we may get more information by + // ignoring the old high if it has decayed too much. We try both and choose the one that gives the lowest + // probability for the new high. + val a = copy(low = newLow, high = newHigh) + val b = copy(low = newLow, high = (maxCapacity - amount).max(0 msat), highTimestamp = timestamp) + if (a.canSend(newHigh) < b.canSend(newHigh)) { + a + } else { + b + } + } else { + copy(low = newLow) + } + } + + /** + * We successfully received the given amount, so we know that some of the liquidity has shifted. + */ + def didReceive(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = + otherSide.didSend(amount, timestamp).otherSide + + def addEdge(edge: GraphEdge): BalanceEstimate = copy( + high = high.max(edge.capacity.toMilliSatoshi), + capacities = capacities.updated(edge.desc.shortChannelId, edge.capacity) + ) + + def removeEdge(desc: ChannelDesc): BalanceEstimate = { + val edgeCapacity = capacities.getOrElse(desc.shortChannelId, 0 sat) + val newCapacities = capacities.removed(desc.shortChannelId) + copy( + low = (low - edgeCapacity.toMilliSatoshi).max(0 msat), + high = high.min(newCapacities.values.maxOption.getOrElse(0 sat).toMilliSatoshi), + capacities = newCapacities + ) + } + + /** + * Estimate the probability that we can successfully send `amount` through the channel + * + * We estimate this probability with a piecewise linear function: + * - probability that it can relay a payment of 0 is 1 + * - probability that it can relay a payment of low is decay(low, 1, lowTimestamp) which is close to 1 if lowTimestamp is recent + * - probability that it can relay a payment of high is decay(high, 0, highTimestamp) which is close to 0 if highTimestamp is recent + * - probability that it can relay a payment of maxCapacity is 0 + */ + def canSend(amount: MilliSatoshi): Double = { + val a = amount.toLong.toDouble + val l = low.toLong.toDouble + val h = high.toLong.toDouble + val c = maxCapacity.toMilliSatoshi.toLong.toDouble + + // Success probability at the low and high points + val pLow = decay(low, 1, lowTimestamp) + val pHigh = decay(high, 0, highTimestamp) + + if (amount < low) { + (l - a * (1.0 - pLow)) / l + } else if (amount < high) { + ((h - a) * pLow + (a - l) * pHigh) / (h - l) + } else if (h < c) { + ((c - a) * pHigh) / (c - h) + } else { + 0 + } + } +} + +object BalanceEstimate { + def empty(halfLife: FiniteDuration): BalanceEstimate = BalanceEstimate(0 msat, 0 unixsec, 0 msat, 0 unixsec, Map.empty, halfLife) +} + +/** + * Balance estimates for the whole routing graph. + */ +case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstimate], defaultHalfLife: FiniteDuration) { + private def get(a: PublicKey, b: PublicKey): Option[BalanceEstimate] = balances.get((a, b)) + + def addEdge(edge: GraphEdge): BalancesEstimates = BalancesEstimates( + balances.updatedWith((edge.desc.a, edge.desc.b))(balance => + Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge)) + ), + defaultHalfLife + ) + + def removeEdge(desc: ChannelDesc): BalancesEstimates = BalancesEstimates( + balances.updatedWith((desc.a, desc.b)) { + case None => None + case Some(balance) => + val newBalance = balance.removeEdge(desc) + if (newBalance.capacities.nonEmpty) { + Some(newBalance) + } else { + None + } + }, + defaultHalfLife + ) + + def channelCouldSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelaySuccess(estimatedProbability) + } + BalancesEstimates(balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.couldSend(amount, TimestampSecond.now()))), defaultHalfLife) + } + + def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelayFailure(estimatedProbability) + } + BalancesEstimates(balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.couldNotSend(amount, TimestampSecond.now()))), defaultHalfLife) + } + + def channelDidSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelaySuccess(estimatedProbability) + } + val balances1 = balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.didSend(amount, TimestampSecond.now()))) + val balances2 = balances1.updatedWith((hop.nextNodeId, hop.nodeId))(_.map(_.didReceive(amount, TimestampSecond.now()))) + BalancesEstimates(balances2, defaultHalfLife) + } + +} + +case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: BalancesEstimates) { + def addEdge(edge: GraphEdge): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addEdge(edge), balances.addEdge(edge)) + + def removeEdge(desc: ChannelDesc): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.removeEdge(desc), balances.removeEdge(desc)) + + def removeEdges(descList: Iterable[ChannelDesc]): GraphWithBalanceEstimates = GraphWithBalanceEstimates( + graph.removeEdges(descList), + descList.foldLeft(balances)((acc, edge) => acc.removeEdge(edge)), + ) + + def routeCouldRelay(route: Route): GraphWithBalanceEstimates = { + val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + case (hop, (balances, amount)) => + (balances.channelCouldSend(hop, amount), amount + hop.fee(amount)) + } + GraphWithBalanceEstimates(graph, balances1) + } + + def routeDidRelay(route: Route): GraphWithBalanceEstimates = { + val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + case (hop, (balances, amount)) => + (balances.channelDidSend(hop, amount), amount + hop.fee(amount)) + } + GraphWithBalanceEstimates(graph, balances1) + } + + def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): GraphWithBalanceEstimates = { + GraphWithBalanceEstimates(graph, balances.channelCouldNotSend(hop, amount)) + } + + def canSend(amount: MilliSatoshi, edge: GraphEdge): Double = { + balances.balances.get((edge.desc.a, edge.desc.b)) match { + case Some(estimate) => estimate.canSend(amount) + case None => BalanceEstimate.empty(1 hour).addEdge(edge).canSend(amount) + } + } +} + +object GraphWithBalanceEstimates { + def apply(graph: DirectedGraph, defaultHalfLife: FiniteDuration): GraphWithBalanceEstimates = { + val balances = graph.edgeSet().foldLeft(Map.empty[(PublicKey, PublicKey), BalanceEstimate]) { + case (m, edge) => m.updatedWith((edge.desc.a, edge.desc.b))(balance => Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge))) + } + GraphWithBalanceEstimates(graph, BalancesEstimates(balances, defaultHalfLife)) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 22adfbc5a2..554a25dbea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -16,13 +16,14 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Btc, ByteVector32, MilliBtc, Satoshi} -import fr.acinq.eclair._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, MilliBtc, Satoshi} import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.payment.Invoice import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.{RealShortChannelId, _} import scala.annotation.tailrec import scala.collection.immutable.SortedMap @@ -65,7 +66,7 @@ object Graph { * @param failureCost fee for a failed attempt * @param hopCost virtual fee per hop (how much we're willing to pay to make the route one hop shorter) */ - case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees) + case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees, useLogProbability: Boolean) case class WeightedNode(key: PublicKey, weight: RichWeight) @@ -246,23 +247,26 @@ object Graph { } neighborEdges.foreach { edge => val neighbor = edge.desc.a - // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that - // will be relayed through that edge is the one in `currentWeight`. - val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost) - val canRelayAmount = current.weight.amount <= edge.capacity && + if (current.weight.amount <= edge.capacity && edge.balance_opt.forall(current.weight.amount <= _) && - edge.update.htlcMaximumMsat.forall(current.weight.amount <= _) && - current.weight.amount >= edge.update.htlcMinimumMsat - if (canRelayAmount && boundaries(neighborWeight) && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor)) { - val previousNeighborWeight = bestWeights.getOrElse(neighbor, RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), 0.0, MilliSatoshi(Long.MaxValue), MilliSatoshi(Long.MaxValue), Double.MaxValue)) - // if this path between neighbor and the target has a shorter distance than previously known, we select it - if (neighborWeight.weight < previousNeighborWeight.weight) { - // update the best edge for this vertex - bestEdges.put(neighbor, edge) - // add this updated node to the list for further exploration - toExplore.enqueue(WeightedNode(neighbor, neighborWeight)) // O(1) - // update the minimum known distance array - bestWeights.put(neighbor, neighborWeight) + edge.params.htlcMaximum_opt.forall(current.weight.amount <= _) && + current.weight.amount >= edge.params.htlcMinimum && + !ignoredEdges.contains(edge.desc) && + !ignoredVertices.contains(neighbor)) { + // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that + // will be relayed through that edge is the one in `currentWeight`. + val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost) + if (boundaries(neighborWeight)) { + val previousNeighborWeight = bestWeights.getOrElse(neighbor, RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), 0.0, MilliSatoshi(Long.MaxValue), MilliSatoshi(Long.MaxValue), Double.MaxValue)) + // if this path between neighbor and the target has a shorter distance than previously known, we select it + if (neighborWeight.weight < previousNeighborWeight.weight) { + // update the best edge for this vertex + bestEdges.put(neighbor, edge) + // add this updated node to the list for further exploration + toExplore.enqueue(WeightedNode(neighbor, neighborWeight)) // O(1) + // update the minimum known distance array + bestWeights.put(neighbor, neighborWeight) + } } } } @@ -299,23 +303,30 @@ object Graph { val totalAmount = if (edge.desc.a == sender && !includeLocalChannelCost) prev.amount else addEdgeFees(edge, prev.amount) val fee = totalAmount - prev.amount val totalFees = prev.fees + fee - val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.update.cltvExpiryDelta + val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.params.cltvExpiryDelta val totalCltv = prev.cltv + cltv weightRatios match { case Left(weightRatios) => - val hopCost = nodeFee(weightRatios.hopCost, prev.amount) + val hopCost = if (edge.desc.a == sender) 0 msat else nodeFee(weightRatios.hopCost, prev.amount) import RoutingHeuristics._ // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. - val channelBlockHeight = ShortChannelId.coordinates(edge.desc.shortChannelId).blockHeight - val ageFactor = normalize(channelBlockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + val ageFactor = edge.desc.shortChannelId match { + case real: RealShortChannelId => normalize(real.blockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + // for local channels or route hints we don't easily have access to the channel block height, but we want to + // give them the best score anyway + case _: Alias => 1 + case _: UnspecifiedShortChannelId => 1 + } // Every edge is weighted by channel capacity, larger channels add less weight val edgeMaxCapacity = edge.capacity.toMilliSatoshi - val capFactor = 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) + val capFactor = + if (edge.balance_opt.isDefined) 0 // If we know the balance of the channel we treat it as if it had the maximum capacity. + else 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) // Every edge is weighted by its cltv-delta value, normalized - val cltvFactor = normalize(edge.update.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) + val cltvFactor = normalize(edge.params.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) // NB we're guaranteed to have weightRatios and factors > 0 val factor = weightRatios.baseFactor + (cltvFactor * weightRatios.cltvDeltaFactor) + (ageFactor * weightRatios.ageFactor) + (capFactor * weightRatios.capacityFactor) @@ -324,17 +335,22 @@ object Graph { case Right(heuristicsConstants) => val hopCost = nodeFee(heuristicsConstants.hopCost, prev.amount) val totalHopsCost = prev.virtualFees + hopCost - val riskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk - // If the edge was added by the invoice, it is assumed that it can route the payment. // If we know the balance of the channel, then we will check separately that it can relay the payment. - val successProbability = if (edge.update.chainHash == ByteVector32.Zeroes || edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + val successProbability = if (edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble if (successProbability < 0) { throw NegativeProbability(edge, prev, heuristicsConstants) } val totalSuccessProbability = prev.successProbability * successProbability val failureCost = nodeFee(heuristicsConstants.failureCost, totalAmount) - val weight = totalFees.toLong + totalHopsCost.toLong + riskCost + failureCost.toLong / totalSuccessProbability - RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + if (heuristicsConstants.useLogProbability) { + val riskCost = totalAmount.toLong * cltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = prev.weight + fee.toLong + hopCost.toLong + riskCost - failureCost.toLong * math.log(successProbability) + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } else { + val totalRiskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = totalFees.toLong + totalHopsCost.toLong + totalRiskCost + failureCost.toLong / totalSuccessProbability + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } } } @@ -347,7 +363,7 @@ object Graph { * @return the new amount updated with the necessary fees for this edge */ private def addEdgeFees(edge: GraphEdge, amountToForward: MilliSatoshi): MilliSatoshi = { - amountToForward + nodeFee(edge.update, amountToForward) + amountToForward + edge.params.fee(amountToForward) } /** Validate that all edges along the path can relay the amount with fees. */ @@ -359,8 +375,8 @@ object Graph { case Some(edge) => val canRelayAmount = amount <= edge.capacity && edge.balance_opt.forall(amount <= _) && - edge.update.htlcMaximumMsat.forall(amount <= _) && - edge.update.htlcMinimumMsat <= amount + edge.params.htlcMaximum_opt.forall(amount <= _) && + edge.params.htlcMinimum <= amount if (canRelayAmount) validateReversePath(path.tail, addEdgeFees(edge, amount)) else false } @@ -399,9 +415,10 @@ object Graph { * extremes but always bigger than zero so it's guaranteed to never return zero */ def normalize(value: Double, min: Double, max: Double): Double = { - if (value <= min) 0.00001D - else if (value > max) 0.99999D - else (value - min) / (max - min) + require(max > min, "The max must be larger than the min") + val clampedValue = Math.min(Math.max(min, value), max) + val extent = max - min + 0.00001D + 0.99998D * (clampedValue - min) / extent } } @@ -412,27 +429,53 @@ object Graph { * Representation of an edge of the graph * * @param desc channel description - * @param update channel info + * @param params source of the channel parameters: can be a channel_update or hints from an invoice * @param capacity channel capacity * @param balance_opt (optional) available balance that can be sent through this edge */ - case class GraphEdge(desc: ChannelDesc, update: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) { + case class GraphEdge private(desc: ChannelDesc, params: ChannelRelayParams, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) { def maxHtlcAmount(reservedCapacity: MilliSatoshi): MilliSatoshi = Seq( balance_opt.map(balance => balance - reservedCapacity), - update.htlcMaximumMsat, + params.htlcMaximum_opt, Some(capacity.toMilliSatoshi - reservedCapacity) ).flatten.min.max(0 msat) - def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(update, amount) + def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount) + } + object GraphEdge { + def apply(u: ChannelUpdate, pc: PublicChannel): GraphEdge = GraphEdge( + desc = ChannelDesc(u, pc.ann), + params = ChannelRelayParams.FromAnnouncement(u), + capacity = pc.capacity, + balance_opt = pc.getBalanceSameSideAs(u) + ) + + def apply(u: ChannelUpdate, pc: PrivateChannel): GraphEdge = GraphEdge( + desc = ChannelDesc(u, pc), + params = ChannelRelayParams.FromAnnouncement(u), + capacity = pc.capacity, + balance_opt = pc.getBalanceSameSideAs(u) + ) + + def apply(e: Invoice.ExtraEdge): GraphEdge = e match { + case e@Invoice.BasicEdge(sourceNodeId, targetNodeId, shortChannelId, _, _, _) => + val maxBtc = 21e6.btc + GraphEdge( + desc = ChannelDesc(shortChannelId, sourceNodeId, targetNodeId), + params = ChannelRelayParams.FromHint(e), + // Bolt 11 routing hints don't include the channel's capacity, so we assume it's big enough + capacity = maxBtc.toSatoshi, + // we assume channels provided as hints have enough balance to handle the payment + balance_opt = Some(maxBtc.toMilliSatoshi) + ) + } } /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ case class DirectedGraph(private val vertices: Map[PublicKey, List[GraphEdge]]) { - def addEdge(d: ChannelDesc, u: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi] = None): DirectedGraph = addEdge(GraphEdge(d, u, capacity, balance_opt)) - def addEdges(edges: Iterable[GraphEdge]): DirectedGraph = edges.foldLeft(this)((acc, edge) => acc.addEdge(edge)) /** @@ -578,33 +621,27 @@ object Graph { * * @param channels map of all known public channels in the network. */ - def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { + def makeGraph(channels: SortedMap[RealShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new mutable.HashMap[PublicKey, List[GraphEdge]](initialCapacity = channels.size + 1, mutable.HashMap.defaultLoadFactor) // add all the vertices and edges in one go channels.values.foreach { channel => - channel.update_1_opt.foreach { u1 => - val desc1 = Router.getDesc(u1, channel.ann) - addDescToMap(desc1, u1, channel.capacity, channel.meta_opt.map(_.balance1)) - } - channel.update_2_opt.foreach { u2 => - val desc2 = Router.getDesc(u2, channel.ann) - addDescToMap(desc2, u2, channel.capacity, channel.meta_opt.map(_.balance2)) - } + channel.update_1_opt.foreach(u1 => addToMap(GraphEdge(u1, channel))) + channel.update_2_opt.foreach(u2 => addToMap(GraphEdge(u2, channel))) } - def addDescToMap(desc: ChannelDesc, u: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi]): Unit = { - mutableMap.put(desc.b, GraphEdge(desc, u, capacity, balance_opt) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) - if (!mutableMap.contains(desc.a)) { - mutableMap += desc.a -> List.empty[GraphEdge] + def addToMap(edge: GraphEdge): Unit = { + mutableMap.put(edge.desc.b, edge +: mutableMap.getOrElse(edge.desc.b, List.empty[GraphEdge])) + if (!mutableMap.contains(edge.desc.a)) { + mutableMap += edge.desc.a -> List.empty[GraphEdge] } } new DirectedGraph(mutableMap.toMap) } - def graphEdgeToHop(graphEdge: GraphEdge): ChannelHop = ChannelHop(graphEdge.desc.a, graphEdge.desc.b, graphEdge.update) + def graphEdgeToHop(graphEdge: GraphEdge): ChannelHop = ChannelHop(graphEdge.desc.shortChannelId, graphEdge.desc.a, graphEdge.desc.b, graphEdge.params) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala index e4c2d2f740..5daf5383ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.{BtcDouble, MilliBtcDouble, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{BtcDouble, MilliBtcDouble, SatoshiLong} import fr.acinq.eclair.router.Router.GossipDecision import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, getSimpleClassName} @@ -66,6 +66,12 @@ object Monitoring { case _: GossipDecision.Accepted => GossipResult.withTag("result", "accepted") case rejected: GossipDecision.Rejected => GossipResult.withTag("result", "rejected").withTag("reason", getSimpleClassName(rejected)) } + + private val RelayProbabilityEstimate = Kamon.histogram("router.balance-estimates.remote-edge-relay", "Estimated probability (in percent) that the relay will be successful") + + def remoteEdgeRelaySuccess(estimatedProbability: Double) = RelayProbabilityEstimate.withTag("success", (estimatedProbability * 100).toLong) + + def remoteEdgeRelayFailure(estimatedProbability: Double) = RelayProbabilityEstimate.withTag("failure", (estimatedProbability * 100).toLong) } object Tags { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala index 50f68cfdff..f960bf8d6b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} @@ -37,7 +38,7 @@ case class SingleChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi, case class ChannelsDiscovered(c: Iterable[SingleChannelDiscovered]) extends NetworkEvent -case class ChannelLost(shortChannelId: ShortChannelId) extends NetworkEvent +case class ChannelLost(shortChannelId: RealShortChannelId) extends NetworkEvent case class ChannelUpdatesReceived(ann: Iterable[ChannelUpdate]) extends NetworkEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index df9b4841cd..b15763634b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -19,17 +19,14 @@ package fr.acinq.eclair.router import akka.actor.{ActorContext, ActorRef, Status} import akka.event.DiagnosticLoggingAdapter import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight, RoutingHeuristics} +import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight} import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.ChannelUpdate import kamon.tag.TagSet import scala.annotation.tailrec @@ -46,11 +43,7 @@ object RouteCalculation { paymentHash_opt = fr.paymentContext.map(_.paymentHash))) { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.route.targetNodeId, fr.amount)).toMap - val extraEdges = assistedChannels.values.map(ac => - GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) - ).toSet - val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } + val g = fr.extraEdges.map(GraphEdge(_)).foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } fr.route match { case PredefinedNodeRoute(hops) => @@ -59,33 +52,34 @@ object RouteCalculation { case edges if edges.nonEmpty && edges.forall(_.nonEmpty) => // select the largest edge (using balance when available, otherwise capacity). val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) - val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update)) + val hops = selectedEdges.map(d => ChannelHop(d.desc.shortChannelId, d.desc.a, d.desc.b, d.params)) ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) case _ => // some nodes in the supplied route aren't connected in our graph ctx.sender() ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) } - case PredefinedChannelRoute(targetNodeId, channels) => - val (end, hops) = channels.foldLeft((localNodeId, Seq.empty[ChannelHop])) { - case ((start, current), shortChannelId) => - val channelDesc_opt = d.channels.get(shortChannelId).flatMap(c => start match { - case c.ann.nodeId1 => Some(ChannelDesc(shortChannelId, c.ann.nodeId1, c.ann.nodeId2)) - case c.ann.nodeId2 => Some(ChannelDesc(shortChannelId, c.ann.nodeId2, c.ann.nodeId1)) - case _ => None - }).orElse(d.privateChannels.get(shortChannelId).flatMap(c => start match { - case c.nodeId1 => Some(ChannelDesc(shortChannelId, c.nodeId1, c.nodeId2)) - case c.nodeId2 => Some(ChannelDesc(shortChannelId, c.nodeId2, c.nodeId1)) - case _ => None - })).orElse(assistedChannels.get(shortChannelId).flatMap(c => start match { - case c.extraHop.nodeId => Some(ChannelDesc(shortChannelId, c.extraHop.nodeId, c.nextNodeId)) - case _ => None - })) + case PredefinedChannelRoute(targetNodeId, shortChannelIds) => + val (end, hops) = shortChannelIds.foldLeft((localNodeId, Seq.empty[ChannelHop])) { + case ((currentNode, previousHops), shortChannelId) => + val channelDesc_opt = d.resolve(shortChannelId) match { + case Some(c: PublicChannel) => currentNode match { + case c.nodeId1 => Some(ChannelDesc(shortChannelId, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(shortChannelId, c.nodeId2, c.nodeId1)) + case _ => None + } + case Some(c: PrivateChannel) => currentNode match { + case c.nodeId1 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId2, c.nodeId1)) + case _ => None + } + case None => fr.extraEdges.map(GraphEdge(_)).find(e => e.desc.shortChannelId == shortChannelId && e.desc.a == currentNode).map(_.desc) + } channelDesc_opt.flatMap(c => g.getEdge(c)) match { - case Some(edge) => (edge.desc.b, current :+ ChannelHop(edge.desc.a, edge.desc.b, edge.update)) - case None => (start, current) + case Some(edge) => (edge.desc.b, previousHops :+ ChannelHop(edge.desc.shortChannelId, edge.desc.a, edge.desc.b, edge.params)) + case None => (currentNode, previousHops) } } - if (end != targetNodeId || hops.length != channels.length) { + if (end != targetNodeId || hops.length != shortChannelIds.length) { ctx.sender() ! Status.Failure(new IllegalArgumentException("The sequence of channels provided cannot be used to build a route to the target node")) } else { ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) @@ -104,26 +98,20 @@ object RouteCalculation { paymentHash_opt = r.paymentContext.map(_.paymentHash))) { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - // we convert extra routing info provided in the payment request to fake channel_update - // it takes precedence over all other channel_updates we know - val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount)) - .filterNot { case (_, ac) => ac.extraHop.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information - .toMap - val extraEdges = assistedChannels.values.map(ac => - GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) - ).toSet + val extraEdges = r.extraEdges.map(GraphEdge(_)).filterNot(_.desc.a == r.source).toSet // we ignore routing hints for our own channels, we have more accurate information val ignoredEdges = r.ignore.channels ++ d.excludedChannels val params = r.routeParams val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) + log.info("local channels to recipient: {}", d.graphWithBalances.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) } else { - findRoute(d.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) + findRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) } result match { case Success(routes) => @@ -135,11 +123,11 @@ object RouteCalculation { Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, "InfiniteLoop")).increment() ctx.sender() ! Status.Failure(failure) case Failure(failure: NegativeProbability) => - log.error(s"computed negative probability $failure") + log.error(s"computed negative probability: edge=${failure.edge}, weight=${failure.weight}, heuristicsConstants=${failure.heuristicsConstants}") Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, "NegativeProbability")).increment() ctx.sender() ! Status.Failure(failure) case Failure(t) => - val failure = if (isNeighborBalanceTooLow(d.graph, r)) BalanceTooLow else t + val failure = if (isNeighborBalanceTooLow(d.graphWithBalances.graph, r)) BalanceTooLow else t Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, failure.getClass.getSimpleName)).increment() ctx.sender() ! Status.Failure(failure) } @@ -148,35 +136,6 @@ object RouteCalculation { } } - private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { - // the `direction` bit in flags will not be accurate but it doesn't matter because it is not used - // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out - ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, TimestampSecond.now(), channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = true), extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) - } - - def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey, amount: MilliSatoshi): Map[ShortChannelId, AssistedChannel] = { - // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination - // The invoice doesn't explicitly specify the channel's htlcMaximumMsat, but we can safely assume that the channel - // should be able to route the payment, so we'll compute an htlcMaximumMsat accordingly. - // We could also get the channel capacity from the blockchain (since we have the shortChannelId) but that's more expensive. - // We also need to make sure the channel isn't excluded by our heuristics. - val lastChannelCapacity = amount.max(RoutingHeuristics.CAPACITY_CHANNEL_LOW) - val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId - extraRoute.zip(nextNodeIds).reverse.foldLeft((lastChannelCapacity, Map.empty[ShortChannelId, AssistedChannel])) { - case ((amount, acs), (extraHop: ExtraHop, nextNodeId)) => - val nextAmount = amount + nodeFee(extraHop.feeBase, extraHop.feeProportionalMillionths, amount) - (nextAmount, acs + (extraHop.shortChannelId -> AssistedChannel(extraHop, nextNodeId, nextAmount))) - }._2 - } - - def toChannelDescs(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[ChannelDesc] = { - val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId - extraRoute.zip(nextNodeIds).map { case (hop, nextNodeId) => ChannelDesc(hop.shortChannelId, hop.nodeId, nextNodeId) } - } - - /** Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount. */ - private def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat - /** This method is used after a payment failed, and we want to exclude some nodes that we know are failing */ def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { val desc = if (ignoreNodes.isEmpty) { @@ -333,7 +292,7 @@ object RouteCalculation { val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { // We should always have balance information available for local channels. // NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended). - case GraphEdge(_, update, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < update.htlcMinimumMsat) + case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum) } // If we have direct channels to the target, we can use them all. // We also count empty channels, which allows replacing them with a non-direct route (multiple hops). @@ -347,7 +306,7 @@ object RouteCalculation { case Right(routes) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1) match { - case Right(routes) if validateMultiPartRoute(amount, maxFee, routes) => Right(routes) + case Right(routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right(routes) case _ => Left(RouteNotFound) } case Left(ex) => Left(ex) @@ -387,12 +346,12 @@ object RouteCalculation { /** Compute the maximum amount that we can send through the given route. */ private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Route = { - val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.update.shortChannelId, 0 msat)) + val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) val amount = route.drop(1).foldLeft(firstHopMaxAmount) { case (amount, edge) => // We compute fees going forward instead of backwards. That means we will slightly overestimate the fees of some // edges, but we will always stay inside the capacity bounds we computed. val amountMinusFees = amount - edge.fee(amount) - val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.update.shortChannelId, 0 msat)) + val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) amountMinusFees.min(edgeMaxAmount) } Route(amount.max(0 msat), route.map(graphEdgeToHop)) @@ -410,14 +369,14 @@ object RouteCalculation { /** Update used capacity by taking into account an HTLC sent to the given route. */ private def updateUsedCapacity(route: Route, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Unit = { route.hops.reverse.foldLeft(route.amount) { case (amount, hop) => - usedCapacity.updateWith(hop.lastUpdate.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) + usedCapacity.updateWith(hop.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) amount + hop.fee(amount) } } - private def validateMultiPartRoute(amount: MilliSatoshi, maxFee: MilliSatoshi, routes: Seq[Route]): Boolean = { + private def validateMultiPartRoute(amount: MilliSatoshi, maxFee: MilliSatoshi, routes: Seq[Route], includeLocalChannelCost: Boolean): Boolean = { val amountOk = routes.map(_.amount).sum == amount - val feeOk = routes.map(_.fee).sum <= maxFee + val feeOk = routes.map(_.fee(includeLocalChannelCost)).sum <= maxFee amountOk && feeOk } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index bdb7409d5b..3327c1856f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -21,8 +21,8 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated, typed} import akka.event.DiagnosticLoggingAdapter import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher @@ -31,13 +31,14 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Invoice.ExtraEdge +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} +import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.wire.protocol._ -import kamon.context.Context import java.util.UUID import scala.collection.immutable.SortedMap @@ -57,9 +58,11 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm // we pass these to helpers classes so that they have the logging context implicit def implicitLog: DiagnosticLoggingAdapter = diagLog + context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) startTimerWithFixedDelay(TickBroadcast.toString, TickBroadcast, nodeParams.routerConf.routerBroadcastInterval) startTimerWithFixedDelay(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 hour) @@ -88,19 +91,29 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm val txid = pc.fundingTxid val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(pc.ann.shortChannelId) // avoid herd effect at startup because watch-spent are intensive in terms of rpc calls to bitcoind - context.system.scheduler.scheduleOnce(Random.nextLong(nodeParams.watchSpentWindow.toSeconds).seconds) { + context.system.scheduler.scheduleOnce(Random.nextLong(nodeParams.routerConf.watchSpentWindow.toSeconds).seconds) { watcher ! WatchExternalChannelSpent(self, txid, outputIndex, pc.ann.shortChannelId) } } // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) self ! nodeAnn log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + val data = Data( + initNodes, initChannels, + Stash(Map.empty, Map.empty), + rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), + awaiting = Map.empty, + privateChannels = Map.empty, + scid2PrivateChannels = Map.empty, + excludedChannels = Set.empty, + graphWithBalances = GraphWithBalanceEstimates(graph, nodeParams.routerConf.balanceEstimateHalfLife), + sync = Map.empty) + startWith(NORMAL, data) } when(NORMAL) { @@ -172,12 +185,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm sender() ! d.nodes.values stay() - case Event(GetLocalChannels, d) => - val scids = d.graph.getIncomingEdgesOf(nodeParams.nodeId).map(_.desc.shortChannelId) - val localChannels = scids.flatMap(scid => d.channels.get(scid).orElse(d.privateChannels.get(scid)).map(c => LocalChannel(nodeParams.nodeId, scid, c))) - sender() ! localChannels - stay() - case Event(GetChannels, d) => sender() ! d.channels.values.map(_.ann) stay() @@ -222,14 +229,17 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(PeerRoutingMessage(peerConnection, remoteNodeId, n: NodeAnnouncement), d: Data) => stay() using Validation.handleNodeAnnouncement(d, nodeParams.db.network, Set(RemoteGossip(peerConnection, remoteNodeId)), n) - case Event(u: ChannelUpdate, d: Data) => + case Event(scia: ShortChannelIdAssigned, d) => + stay() using Validation.handleShortChannelIdAssigned(d, nodeParams.nodeId, scia) + + case Event(u: ChannelUpdate, d: Data) => // from payment lifecycle stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(LocalGossip)))) - case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => + case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => // from network (gossip or peer) stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(RemoteGossip(peerConnection, remoteNodeId))))) - case Event(lcu: LocalChannelUpdate, d: Data) => - stay() using Validation.handleLocalChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, nodeParams.nodeId, watcher, lcu) + case Event(lcu: LocalChannelUpdate, d: Data) => // from local channel + stay() using Validation.handleLocalChannelUpdate(d, nodeParams, watcher, lcu) case Event(lcd: LocalChannelDown, d: Data) => stay() using Validation.handleLocalChannelDown(d, nodeParams.nodeId, lcd) @@ -254,26 +264,35 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(PeerRoutingMessage(peerConnection, remoteNodeId, r: ReplyShortChannelIdsEnd), d) => stay() using Sync.handleReplyShortChannelIdsEnd(d, RemoteGossip(peerConnection, remoteNodeId), r) + case Event(RouteCouldRelay(route), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.routeCouldRelay(route)) + + case Event(RouteDidRelay(route), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.routeDidRelay(route)) + + case Event(ChannelCouldNotRelay(amount, hop), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.channelCouldNotSend(hop, amount)) } initialize() override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) - currentMessage match { - case s: SendChannelQuery => Logs.mdc(category_opt, remoteNodeId_opt = Some(s.remoteNodeId)) - case prm: PeerRoutingMessage => Logs.mdc(category_opt, remoteNodeId_opt = Some(prm.remoteNodeId)) - case lcu: LocalChannelUpdate => Logs.mdc(category_opt, remoteNodeId_opt = Some(lcu.remoteNodeId)) - case _ => Logs.mdc(category_opt) + val (remoteNodeId_opt, channelId_opt) = currentMessage match { + case s: SendChannelQuery => (Some(s.remoteNodeId), None) + case prm: PeerRoutingMessage => (Some(prm.remoteNodeId), None) + case sca: ShortChannelIdAssigned => (Some(sca.remoteNodeId), Some(sca.channelId)) + case lcu: LocalChannelUpdate => (Some(lcu.remoteNodeId), Some(lcu.channelId)) + case lcd: LocalChannelDown => (Some(lcd.remoteNodeId), Some(lcd.channelId)) + case abc: AvailableBalanceChanged => (Some(abc.commitments.remoteNodeId), Some(abc.channelId)) + case _ => (None, None) } + Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, channelId_opt = channelId_opt, nodeAlias_opt = Some(nodeParams.alias)) } } object Router { - val shortChannelIdKey = Context.key[ShortChannelId]("shortChannelId", ShortChannelId(0)) - val remoteNodeIdKey = Context.key[String]("remoteNodeId", "unknown") - def props(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) case class SearchBoundaries(maxFeeFlat: MilliSatoshi, @@ -297,30 +316,50 @@ object Router { ) } - case class RouterConf(channelExcludeDuration: FiniteDuration, + case class RouterConf(watchSpentWindow: FiniteDuration, + channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, requestNodeAnnouncements: Boolean, encodingType: EncodingType, channelRangeChunkSize: Int, channelQueryChunkSize: Int, - pathFindingExperimentConf: PathFindingExperimentConf) + pathFindingExperimentConf: PathFindingExperimentConf, + balanceEstimateHalfLife: FiniteDuration) { + require(channelRangeChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel range chunk size exceeds the size of a lightning message") + require(channelQueryChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel query chunk size exceeds the size of a lightning message") + } // @formatter:off - case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) + case class ChannelDesc private(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) + object ChannelDesc { + def apply(u: ChannelUpdate, ann: ChannelAnnouncement): ChannelDesc = { + // the least significant bit tells us if it is node1 or node2 + if (u.channelFlags.isNode1) ChannelDesc(ann.shortChannelId, ann.nodeId1, ann.nodeId2) else ChannelDesc(ann.shortChannelId, ann.nodeId2, ann.nodeId1) + } + def apply(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { + // the least significant bit tells us if it is node1 or node2 + if (u.channelFlags.isNode1) ChannelDesc(pc.shortIds.localAlias, pc.nodeId1, pc.nodeId2) else ChannelDesc(pc.shortIds.localAlias, pc.nodeId2, pc.nodeId1) + } + } case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) - sealed trait ChannelDetails { + sealed trait KnownChannel { val capacity: Satoshi + val nodeId1: PublicKey + val nodeId2: PublicKey def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] - def updateChannelUpdateSameSideAs(u: ChannelUpdate): ChannelDetails - def updateBalances(commitments: AbstractCommitments): ChannelDetails - def applyChannelUpdate(update: Either[LocalChannelUpdate, RemoteChannelUpdate]): ChannelDetails + def updateChannelUpdateSameSideAs(u: ChannelUpdate): KnownChannel + def updateBalances(commitments: AbstractCommitments): KnownChannel + def applyChannelUpdate(update: Either[LocalChannelUpdate, RemoteChannelUpdate]): KnownChannel } - case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta_opt: Option[ChannelMeta]) extends ChannelDetails { + case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta_opt: Option[ChannelMeta]) extends KnownChannel { update_1_opt.foreach(u => assert(u.channelFlags.isNode1)) update_2_opt.foreach(u => assert(!u.channelFlags.isNode1)) + val nodeId1: PublicKey = ann.nodeId1 + val nodeId2: PublicKey = ann.nodeId2 + def shortChannelId: RealShortChannelId = ann.shortChannelId def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (u.channelFlags.isNode1) ann.nodeId1 else ann.nodeId2 def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (u.channelFlags.isNode1) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (u.channelFlags.isNode1) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) @@ -335,7 +374,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends ChannelDetails { + case class PrivateChannel(channelId: ByteVector32, shortIds: ShortIds, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) val capacity: Satoshi = (meta.balance1 + meta.balance2).truncateToSatoshi @@ -352,29 +391,22 @@ object Router { case Left(lcu) => updateChannelUpdateSameSideAs(lcu.channelUpdate).updateBalances(lcu.commitments) case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } - } - case class LocalChannel(localNodeId: PublicKey, shortChannelId: ShortChannelId, channel: ChannelDetails) { - val isPrivate: Boolean = channel match { - case _: PrivateChannel => true - case _ => false - } - val capacity: Satoshi = channel.capacity - val remoteNodeId: PublicKey = channel match { - case c: PrivateChannel => c.remoteNodeId - case c: PublicChannel => if (c.ann.nodeId1 == localNodeId) c.ann.nodeId2 else c.ann.nodeId1 - } - /** Our remote peer's channel_update: this is what must be used in invoice routing hints. */ - val remoteUpdate: Option[ChannelUpdate] = channel match { - case c: PrivateChannel => if (remoteNodeId == c.nodeId1) c.update_1_opt else c.update_2_opt - case c: PublicChannel => if (remoteNodeId == c.ann.nodeId1) c.update_1_opt else c.update_2_opt - } /** Create an invoice routing hint from that channel. Note that if the channel is private, the invoice will leak its existence. */ - def toExtraHop: Option[ExtraHop] = remoteUpdate.map(u => ExtraHop(remoteNodeId, u.shortChannelId, u.feeBaseMsat, u.feeProportionalMillionths, u.cltvExpiryDelta)) + def toIncomingExtraHop: Option[Bolt11Invoice.ExtraHop] = { + // we want the incoming channel_update + val remoteUpdate_opt = if (localNodeId == nodeId1) update_2_opt else update_1_opt + // for incoming payments we preferably use the *remote alias*, otherwise the real scid if we have it + val scid_opt = shortIds.remoteAlias_opt.orElse(shortIds.real.toOption) + // we override the remote update's scid, because it contains either the real scid or our local alias + scid_opt.flatMap { scid => + remoteUpdate_opt.map { remoteUpdate => + Bolt11Invoice.ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) + } + } + } } // @formatter:on - case class AssistedChannel(extraHop: ExtraHop, nextNodeId: PublicKey, htlcMaximum: MilliSatoshi) - trait Hop { /** @return the id of the start node. */ def nodeId: PublicKey @@ -392,17 +424,52 @@ object Router { def cltvExpiryDelta: CltvExpiryDelta } + // @formatter:off + /** Channel routing parameters */ + sealed trait ChannelRelayParams { + def cltvExpiryDelta: CltvExpiryDelta + def relayFees: Relayer.RelayFees + final def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(relayFees, amount) + def htlcMinimum: MilliSatoshi + def htlcMaximum_opt: Option[MilliSatoshi] + } + + object ChannelRelayParams { + /** We learnt about this channel from a channel_update */ + case class FromAnnouncement(channelUpdate: ChannelUpdate) extends ChannelRelayParams { + override def cltvExpiryDelta: CltvExpiryDelta = channelUpdate.cltvExpiryDelta + override def relayFees: Relayer.RelayFees = channelUpdate.relayFees + override def htlcMinimum: MilliSatoshi = channelUpdate.htlcMinimumMsat + override def htlcMaximum_opt: Option[MilliSatoshi] = channelUpdate.htlcMaximumMsat + } + /** We learnt about this channel from hints in an invoice */ + case class FromHint(extraHop: Invoice.ExtraEdge) extends ChannelRelayParams { + override def cltvExpiryDelta: CltvExpiryDelta = extraHop.cltvExpiryDelta + override def relayFees: Relayer.RelayFees = extraHop.relayFees + override def htlcMinimum: MilliSatoshi = extraHop.htlcMinimum + override def htlcMaximum_opt: Option[MilliSatoshi] = extraHop.htlcMaximum_opt + } + + def areSame(a: ChannelRelayParams, b: ChannelRelayParams, ignoreHtlcSize: Boolean = false): Boolean = + a.cltvExpiryDelta == b.cltvExpiryDelta && + a.relayFees == b.relayFees && + (ignoreHtlcSize || (a.htlcMinimum == b.htlcMinimum && a.htlcMaximum_opt == b.htlcMaximum_opt)) + } + // @formatter:on + /** * A directed hop between two connected nodes using a specific channel. * - * @param nodeId id of the start node. - * @param nextNodeId id of the end node. - * @param lastUpdate last update of the channel used for the hop. + * @param nodeId id of the start node. + * @param nextNodeId id of the end node. + * @param shortChannelId scid that will be used to build the payment onion. + * @param params source for the channel parameters. */ - case class ChannelHop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends Hop { - override lazy val cltvExpiryDelta: CltvExpiryDelta = lastUpdate.cltvExpiryDelta - - override def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(lastUpdate, amount) + case class ChannelHop(shortChannelId: ShortChannelId, nodeId: PublicKey, nextNodeId: PublicKey, params: ChannelRelayParams) extends Hop { + // @formatter:off + override def cltvExpiryDelta: CltvExpiryDelta = params.cltvExpiryDelta + override def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount) + // @formatter:on } /** @@ -451,7 +518,7 @@ object Router { target: PublicKey, amount: MilliSatoshi, maxFee: MilliSatoshi, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, ignore: Ignore = Ignore.empty, routeParams: RouteParams, allowMultiPart: Boolean = false, @@ -460,7 +527,7 @@ object Router { case class FinalizeRoute(amount: MilliSatoshi, route: PredefinedRoute, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, paymentContext: Option[PaymentContext] = None) /** @@ -472,18 +539,24 @@ object Router { require(hops.nonEmpty, "route cannot be empty") val length = hops.length - lazy val fee: MilliSatoshi = { - val amountToSend = hops.drop(1).reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + + def fee(includeLocalChannelCost: Boolean): MilliSatoshi = { + val hopsToPay = if (includeLocalChannelCost) hops else hops.drop(1) + val amountToSend = hopsToPay.reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } amountToSend - amount } /** This method retrieves the channel update that we used when we built the route. */ - def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) + def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.params).collect { case s: ChannelRelayParams.FromAnnouncement => s.channelUpdate } def printNodes(): String = hops.map(_.nextNodeId).mkString("->") - def printChannels(): String = hops.map(_.lastUpdate.shortChannelId).mkString("->") + def printChannels(): String = hops.map(_.shortChannelId).mkString("->") + def stopAt(nodeId: PublicKey): Route = { + val amountAtStop = hops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + Route(amountAtStop, hops.takeWhile(_.nodeId != nodeId)) + } } case class RouteResponse(routes: Seq[Route]) { @@ -555,7 +628,7 @@ object Router { case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[GossipOrigin]], updates: Map[ChannelUpdate, Set[GossipOrigin]], nodes: Map[NodeAnnouncement, Set[GossipOrigin]]) // @formatter:on - case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long) + case class ShortChannelIdAndFlag(shortChannelId: RealShortChannelId, flag: Long) /** * @param remainingQueries remaining queries to send, the next one will be popped after we receive a [[ReplyShortChannelIdsEnd]] @@ -566,15 +639,33 @@ object Router { } case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], stash: Stash, rebroadcast: Rebroadcast, - awaiting: Map[ChannelAnnouncement, Seq[RemoteGossip]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done - privateChannels: Map[ShortChannelId, PrivateChannel], + awaiting: Map[ChannelAnnouncement, Seq[GossipOrigin]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done + privateChannels: Map[ByteVector32, PrivateChannel], // indexed by channel id + scid2PrivateChannels: Map[Long, ByteVector32], // real scid or alias to channel_id, only to be used for private channels excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure - graph: DirectedGraph, + graphWithBalances: GraphWithBalanceEstimates, sync: Map[PublicKey, Syncing] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query for which we have not yet received an 'end' message - ) + ) { + def resolve(scid: ShortChannelId): Option[KnownChannel] = { + // let's assume this is a real scid + channels.get(RealShortChannelId(scid.toLong)) match { + case Some(publicChannel) => Some(publicChannel) + case None => + // maybe it's an alias or a real scid + scid2PrivateChannels.get(scid.toLong).flatMap(privateChannels.get) match { + case Some(privateChannel) => Some(privateChannel) + case None => None + } + } + } + + def resolve(channelId: ByteVector32, realScid_opt: Option[RealShortChannelId]): Option[KnownChannel] = { + privateChannels.get(channelId).orElse(realScid_opt.flatMap(channels.get)) + } + } // @formatter:off sealed trait State @@ -584,17 +675,16 @@ object Router { case object TickPruneStaleChannels // @formatter:on - def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = { - // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1) - } - - def getDesc(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { - // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) - } - def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 def hasChannels(nodeId: PublicKey, channels: Iterable[PublicChannel]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) + + /** We know that this route could relay because we have tried it but the payment was eventually cancelled */ + case class RouteCouldRelay(route: Route) + + /** We have relayed using this route. */ + case class RouteDidRelay(route: Route) + + /** We have tried to relay this amount from this channel and it failed. */ + case class ChannelCouldNotRelay(amount: MilliSatoshi, hop: ChannelHop) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala index 1eff789583..8905989562 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala @@ -21,7 +21,7 @@ import akka.event.LoggingAdapter import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Router.{ChannelDesc, Data, PublicChannel, hasChannels} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TimestampSecond, TxCoordinates} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, ShortChannelId, TimestampSecond, TxCoordinates} import scala.collection.mutable import scala.concurrent.duration._ @@ -53,14 +53,14 @@ object StaleChannels { staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId2, ca.ann.nodeId1) }) - val graph1 = d.graph.removeEdges(staleChannelsToRemove) + val graphWithBalances1 = d.graphWithBalances.removeEdges(staleChannelsToRemove) staleNodes.foreach { nodeId => log.info("pruning nodeId={} (stale)", nodeId) db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graph = graph1) + d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graphWithBalances = graphWithBalances1) } def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index cefb61c69a..4eadbb4988 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair.router import akka.actor.{ActorContext, ActorRef} import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ @@ -38,9 +39,9 @@ object Sync { // maximum number of ids we can keep in a single chunk and still have an encoded reply that is smaller than 65Kb // please note that: // - this is based on the worst case scenario where peer want timestamps and checksums and the reply is not compressed - // - the maximum number of public channels in a single block so far is less than 300, and the maximum number of tx per block - // almost never exceeds 2800 so this is not a real limitation yet - val MAXIMUM_CHUNK_SIZE = 3200 + // - the maximum number of public channels in a single block so far is less than 300, and the maximum number of tx per + // block almost never exceeds 2800 so this should very rarely be limiting + val MAXIMUM_CHUNK_SIZE = 2700 def handleSendChannelQuery(d: Data, s: SendChannelQuery)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors @@ -69,13 +70,13 @@ object Sync { } } - def handleQueryChannelRange(channels: SortedMap[ShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryChannelRange(channels: SortedMap[RealShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) Metrics.QueryChannelRange.Blocks.withoutTags().record(q.numberOfBlocks) log.info("received query_channel_range with firstBlockNum={} numberOfBlocks={} extendedQueryFlags_opt={}", q.firstBlock, q.numberOfBlocks, q.tlvStream) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) + val shortChannelIds: SortedSet[RealShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, q.firstBlock, q.numberOfBlocks) val chunks = split(shortChannelIds, q.firstBlock, q.numberOfBlocks, routerConf.channelRangeChunkSize) Metrics.QueryChannelRange.Replies.withoutTags().record(chunks.size) @@ -107,7 +108,7 @@ object Sync { Metrics.ReplyChannelRange.ShortChannelIds.withTag(Tags.Direction, Tags.Directions.Incoming).record(r.shortChannelIds.array.size) @tailrec - def loop(ids: List[ShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { + def loop(ids: List[RealShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { ids match { case Nil => acc.reverse case head :: tail => @@ -158,7 +159,7 @@ object Sync { } } - def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[RealShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) @@ -218,7 +219,7 @@ object Sync { /** * Filters channels that we want to send to nodes asking for a channel range */ - def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: ShortChannelId): Boolean = { + def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: RealShortChannelId): Boolean = { val height = id.blockHeight height >= firstBlock && height < (firstBlock + numberOfBlocks) } @@ -251,8 +252,8 @@ object Sync { } } - def computeFlag(channels: SortedMap[ShortChannelId, PublicChannel])( - shortChannelId: ShortChannelId, + def computeFlag(channels: SortedMap[RealShortChannelId, PublicChannel])( + shortChannelId: RealShortChannelId, theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums], includeNodeAnnouncements: Boolean): Long = { @@ -291,8 +292,8 @@ object Sync { * */ def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel])( - ids: List[ShortChannelId], + channels: SortedMap[RealShortChannelId, PublicChannel])( + ids: List[RealShortChannelId], flags: List[Long], onChannel: ChannelAnnouncement => Unit, onUpdate: ChannelUpdate => Unit, @@ -302,7 +303,7 @@ object Sync { // we loop over channel ids and query flag. We track node Ids for node announcement // we've already sent to avoid sending them multiple times, as requested by the BOLTs @tailrec - def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { + def loop(ids: List[RealShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { case Nil => (numca, numcu, nodesSent.size) case head :: tail if !channels.contains(head) => log.warning("received query for shortChannelId={} that we don't have", head) @@ -369,7 +370,7 @@ object Sync { } } - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, PublicChannel])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { + def getChannelDigestInfo(channels: SortedMap[RealShortChannelId, PublicChannel])(shortChannelId: RealShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { val c = channels(shortChannelId) val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L unixsec) val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L unixsec) @@ -390,7 +391,7 @@ object Sync { crc32c(data) } - case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[ShortChannelId]) { + case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[RealShortChannelId]) { /** * @param maximumSize maximum size of the short channel ids list * @return a chunk with at most `maximumSize` ids @@ -419,7 +420,7 @@ object Sync { * returned chunks may still contain more than `channelRangeChunkSize` elements * @return a list of short channel id chunks */ - def split(shortChannelIds: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { + def split(shortChannelIds: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { // see BOLT7: MUST encode a short_channel_id for every open channel it knows in blocks first_blocknum to first_blocknum plus number_of_blocks minus one val it = shortChannelIds.iterator.dropWhile(_.blockHeight < firstBlock).takeWhile(_.blockHeight < firstBlock + numberOfBlocks) if (it.isEmpty) { @@ -429,7 +430,7 @@ object Sync { // ids that have the same block height must be grouped in the same chunk // chunk should contain `channelRangeChunkSize` ids @tailrec - def loop(currentChunk: List[ShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { + def loop(currentChunk: List[RealShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { if (it.hasNext) { val id = it.next() val currentHeight = currentChunk.head.blockHeight @@ -481,7 +482,7 @@ object Sync { * @param channels channels map * @return a ReplyChannelRange object */ - def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = { + def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[RealShortChannelId, PublicChannel]): ReplyChannelRange = { val encoding = if (chunk.shortChannelIds.isEmpty) EncodingType.UNCOMPRESSED else defaultEncoding val (timestamps, checksums) = queryFlags_opt match { case Some(extension) if extension.wantChecksums | extension.wantTimestamps => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index a78c9e9254..b55356a422 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -19,18 +19,21 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{AvailableBalanceChanged, LocalChannelDown, LocalChannelUpdate} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, ShortChannelId, TxCoordinates} +import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TxCoordinates} object Validation { @@ -78,19 +81,21 @@ object Validation { def handleChannelValidationResponse(d0: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], r: ValidateResult)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - import nodeParams.db.{network => db} import r.c - d0.awaiting.get(c) match { - case Some(origin +: _) => origin.peerConnection ! TransportHandler.ReadAck(c) // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement + // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement + // (the other ones have already been acknowledged as duplicates) + d0.awaiting.getOrElse(c, Seq.empty).headOption match { + case Some(origin: RemoteGossip) => origin.peerConnection ! TransportHandler.ReadAck(c) + case Some(LocalGossip) => () // there is nothing to ack if it was a local gossip case _ => () } - val remoteOrigins_opt = d0.awaiting.get(c) - Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins_opt.flatMap(_.headOption).map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first + val remoteOrigins = d0.awaiting.getOrElse(c, Set.empty).collect { case rg: RemoteGossip => rg } + Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins.headOption.map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first log.debug("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) - val publicChannel_opt = r match { + val d1_opt = r match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c))) None case ValidateResult(c, Right((tx, UtxoStatus.Unspent))) => val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) @@ -102,39 +107,28 @@ object Validation { } if (fundingOutputIsInvalid) { log.error(s"invalid script for shortChannelId={}: txid={} does not have script=$fundingOutputScript at outputIndex=$outputIndex ann={}", c.shortChannelId, tx.txid, c) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c))) None } else { - watcher ! WatchExternalChannelSpent(ctx.self, tx.txid, outputIndex, c.shortChannelId) - log.debug("added channel channelId={}", c.shortChannelId) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c)))) + log.debug("validation successful for shortChannelId={}", c.shortChannelId) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))) val capacity = tx.txOut(outputIndex).amount - ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(c, capacity, None, None) :: Nil)) - db.addChannel(c, tx.txid, capacity) - // in case we just validated our first local channel, we announce the local node - if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { - log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) - ctx.self ! nodeAnn - } - // public channels that haven't yet been announced are considered as private channels - val channelMeta_opt = d0.privateChannels.get(c.shortChannelId).map(_.meta) - Some(PublicChannel(c, tx.txid, capacity, None, None, channelMeta_opt)) + Some(addPublicChannel(d0, nodeParams, watcher, c, tx.txid, capacity, None)) } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) // the funding tx has been spent by a transaction that is now confirmed: peer shouldn't send us those - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c))) } else { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c)))) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c))) } // there may be a record if we have just restarted - db.removeChannel(c.shortChannelId) + nodeParams.db.network.removeChannel(c.shortChannelId) None } - // we also reprocess node and channel_update announcements related to channels that were just analyzed + // we also reprocess node and channel_update announcements related to the channel that was just analyzed val reprocessUpdates = d0.stash.updates.view.filterKeys(u => u.shortChannelId == c.shortChannelId) val reprocessNodes = d0.stash.nodes.view.filterKeys(n => isRelatedTo(c, n.nodeId)) // and we remove the reprocessed messages from the stash @@ -142,25 +136,19 @@ object Validation { // we remove channel from awaiting map val awaiting1 = d0.awaiting - c - publicChannel_opt match { - case Some(pc) => - // note: if the channel is graduating from private to public, the implementation (in the LocalChannelUpdate handler) guarantees that we will process a new channel_update - // right after the channel_announcement, channel_updates will be moved from private to public at that time - val d1 = d0.copy( - channels = d0.channels + (c.shortChannelId -> pc), - privateChannels = d0.privateChannels - c.shortChannelId, // we remove fake announcements that we may have made before - rebroadcast = d0.rebroadcast.copy(channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet)), // we also add the newly validated channels to the rebroadcast queue - stash = stash1, - awaiting = awaiting1) - // we only reprocess updates and nodes if validation succeeded - val d2 = reprocessUpdates.foldLeft(d1) { + d1_opt match { + case Some(d1) => + val d2 = d1.copy(stash = stash1, awaiting = awaiting1) + // we process channel updates and node announcements if validation succeeded + val d3 = reprocessUpdates.foldLeft(d2) { case (d, (u, origins)) => Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, origins)), wasStashed = true) } - val d3 = reprocessNodes.foldLeft(d2) { + val d4 = reprocessNodes.foldLeft(d3) { case (d, (n, origins)) => Validation.handleNodeAnnouncement(d, nodeParams.db.network, origins, n, wasStashed = true) } - d3 + d4 case None => + // if validation failed we can fast-discard related announcements reprocessUpdates.foreach { case (u, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoRelatedChannel(u)) } } reprocessNodes.foreach { case (n, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoKnownChannel(n)) } } d0.copy(stash = stash1, awaiting = awaiting1) @@ -168,18 +156,77 @@ object Validation { } } - def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: ShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + private def addPublicChannel(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, privChan_opt: Option[PrivateChannel])(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { + implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors + val fundingOutputIndex = outputIndex(ann.shortChannelId) + watcher ! WatchExternalChannelSpent(ctx.self, fundingTxid, fundingOutputIndex, ann.shortChannelId) + ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(ann, capacity, None, None) :: Nil)) + nodeParams.db.network.addChannel(ann, fundingTxid, capacity) + val pubChan = PublicChannel( + ann = ann, + fundingTxid = fundingTxid, + capacity = capacity, + update_1_opt = privChan_opt.flatMap(_.update_1_opt), + update_2_opt = privChan_opt.flatMap(_.update_2_opt), + meta_opt = privChan_opt.map(_.meta) + ) + log.debug("adding public channel realScid={} localChannel={} publicChannel={}", ann.shortChannelId, privChan_opt.isDefined, pubChan) + // if this is a local channel graduating from private to public, we need to update the graph because the edge + // identifiers change from alias to real scid, and we can also populate the metadata + val graph1 = privChan_opt match { + case Some(privateChannel) => + log.debug("updating the graph for shortChannelId={}", pubChan.shortChannelId) + // mutable variable is simpler here + var graph = d.graphWithBalances + // remove previous private edges + pubChan.update_1_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + pubChan.update_2_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + // add new public edges + pubChan.update_1_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + pubChan.update_2_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + graph + case None => d.graphWithBalances + } + // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid + val rebroadcastUpdates1 = (pubChan.update_1_opt.toSet ++ pubChan.update_2_opt.toSet) + .filter(_.shortChannelId == pubChan.shortChannelId) + .map(u => u -> (if (pubChan.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) + .toMap + val d1 = d.copy( + channels = d.channels + (pubChan.shortChannelId -> pubChan), + // we remove the corresponding unannounced channel that we may have until now + privateChannels = d.privateChannels -- privChan_opt.map(_.channelId).toSeq, + // we also remove the scid -> channelId mappings + scid2PrivateChannels = d.scid2PrivateChannels - pubChan.shortChannelId.toLong -- privChan_opt.map(_.shortIds.localAlias.toLong), + // we also add the newly validated channels to the rebroadcast queue + rebroadcast = d.rebroadcast.copy( + // we rebroadcast the channel to our peers + channels = d.rebroadcast.channels + (pubChan.ann -> d.awaiting.getOrElse(pubChan.ann, if (pubChan.nodeId1 == nodeParams.nodeId || pubChan.nodeId2 == nodeParams.nodeId) Seq(LocalGossip) else Nil).toSet), + // those updates are only defined if the channel was previously an unannounced local channel, we broadcast them + updates = d.rebroadcast.updates ++ rebroadcastUpdates1 + ), + graphWithBalances = graph1 + ) + // in case this was our first local channel, we make a node announcement + if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) { + log.info("first local channel validated, announcing local node") + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) + handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn) + } else d1 + } + + def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: RealShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors val lostChannel = d.channels(shortChannelId).ann log.info("funding tx of channelId={} has been spent", shortChannelId) // we need to remove nodes that aren't tied to any channels anymore - val channels1 = d.channels - lostChannel.shortChannelId + val channels1 = d.channels - shortChannelId val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values)) // let's clean the db and send the events log.info("pruning shortChannelId={} (spent)", shortChannelId) db.removeChannel(shortChannelId) // NB: this also removes channel updates // we also need to remove updates from the graph - val graph1 = d.graph + val graphWithBalances1 = d.graphWithBalances .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId1, lostChannel.nodeId2)) .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId2, lostChannel.nodeId1)) @@ -190,12 +237,12 @@ object Validation { db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graph = graph1) + d.copy(nodes = d.nodes -- lostNodes, channels = channels1, graphWithBalances = graphWithBalances1) } def handleNodeAnnouncement(d: Data, db: NetworkDb, origins: Set[GossipOrigin], n: NodeAnnouncement, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val remoteOrigins = origins flatMap { + val remoteOrigins = origins.flatMap { case r: RemoteGossip if wasStashed => Some(r.peerConnection) case RemoteGossip(peerConnection, _) => @@ -215,7 +262,7 @@ object Validation { remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n))) val origins1 = d.rebroadcast.nodes(n) ++ origins d.copy(rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins1))) - } else if (d.nodes.contains(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) { + } else if (d.nodes.get(n.nodeId).exists(_.timestamp >= n.timestamp)) { log.debug("ignoring {} (duplicate)", n) remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(n))) d @@ -249,235 +296,277 @@ object Validation { def handleChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, update: Either[LocalChannelUpdate, RemoteChannelUpdate], wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val (u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { - case Left(lcu) => (lcu.channelUpdate, Set(LocalGossip)) + val (pc_opt: Option[KnownChannel], u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { + case Left(lcu) => (d.resolve(lcu.channelId, lcu.shortIds.real.toOption), lcu.channelUpdate, Set(LocalGossip)) case Right(rcu) => rcu.origins.collect { case RemoteGossip(peerConnection, _) if !wasStashed => // stashed changes have already been acknowledged log.debug("received channel update for shortChannelId={}", rcu.channelUpdate.shortChannelId) peerConnection ! TransportHandler.ReadAck(rcu.channelUpdate) } - (rcu.channelUpdate, rcu.origins) + (d.resolve(rcu.channelUpdate.shortChannelId), rcu.channelUpdate, rcu.origins) } - if (d.channels.contains(u.shortChannelId)) { - // related channel is already known (note: this means no related channel_update is in the stash) - val publicChannel = true - val pc = d.channels(u.shortChannelId) - val desc = getDesc(u, pc.ann) - if (d.rebroadcast.updates.contains(u)) { - log.debug("ignoring {} (pending rebroadcast)", u) - sendDecision(origins, GossipDecision.Accepted(u)) - val origins1 = d.rebroadcast.updates(u) ++ origins - // NB: we update the channels because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) - d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) - } else if (StaleChannels.isStale(u)) { - log.debug("ignoring {} (stale)", u) - sendDecision(origins, GossipDecision.Stale(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { - log.debug("ignoring {} (duplicate)", u) - sendDecision(origins, GossipDecision.Duplicate(u)) - update match { - case Left(_) => - // NB: we update the graph because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) - d.copy(channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) - case Right(_) => d + pc_opt match { + case Some(pc: PublicChannel) => + // related channel is already known (note: this means no related channel_update is in the stash) + val publicChannel = true + if (d.rebroadcast.updates.contains(u)) { + log.debug("ignoring {} (pending rebroadcast)", u) + sendDecision(origins, GossipDecision.Accepted(u)) + val origins1 = d.rebroadcast.updates(u) ++ origins + update match { + case Left(_) => + // NB: we update the channels because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + case Right(_) => + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1))) + } + } else if (StaleChannels.isStale(u)) { + log.debug("ignoring {} (stale)", u) + sendDecision(origins, GossipDecision.Stale(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).exists(previous => previous.timestamp >= u.timestamp && previous.shortChannelId == u.shortChannelId)) { // NB: we also check the id because there could be a switch alias->real scid + log.debug("ignoring {} (duplicate)", u) + sendDecision(origins, GossipDecision.Duplicate(u)) + update match { + case Left(_) => + // NB: we update the graph because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + case Right(_) => d + } + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { + log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) + sendDecision(origins, GossipDecision.InvalidSignature(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { + log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) + Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + db.updateChannel(u) + // update the graph + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) + d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + } else { + update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) + d.graphWithBalances.removeEdge(ChannelDesc(u, pc1.ann)) + } + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) + } else { + log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + db.updateChannel(u) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) } - } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { - log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) - sendDecision(origins, GossipDecision.InvalidSignature(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) - // update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { - d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + case Some(pc: PrivateChannel) => + val publicChannel = false + if (StaleChannels.isStale(u)) { + log.debug("ignoring {} (stale)", u) + sendDecision(origins, GossipDecision.Stale(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).exists(previous => previous.timestamp >= u.timestamp && previous.shortChannelId == u.shortChannelId)) { // NB: we also check the id because there could be a switch alias->real scid + log.debug("ignoring {} (already know same or newer)", u) + sendDecision(origins, GossipDecision.Duplicate(u)) + d + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { + log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) + sendDecision(origins, GossipDecision.InvalidSignature(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { + log.debug("updated channel_update for channelId={} public={} flags={} {}", pc.channelId, publicChannel, u.channelFlags, u) + Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) + d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + } else { + update.left.foreach(_ => log.info("removed local channelId={} public={} from the network graph", pc.channelId, publicChannel)) + d.graphWithBalances.removeEdge(ChannelDesc(u, pc1)) + } + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graphWithBalances = graphWithBalances1) } else { - d.graph.removeEdge(desc) + log.debug("added channel_update for channelId={} public={} flags={} {}", pc.channelId, publicChannel, u.channelFlags, u) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graphWithBalances = graphWithBalances1) } - d.copy(channels = d.channels + (u.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) - } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) - d.copy(channels = d.channels + (u.shortChannelId -> pc1), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) - } - } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { - // channel is currently being validated - if (d.stash.updates.contains(u)) { - log.debug("ignoring {} (already stashed)", u) - val origins1 = d.stash.updates(u) ++ origins - d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins1))) - } else { - log.debug("stashing {}", u) - d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) - } - } else if (d.privateChannels.contains(u.shortChannelId)) { - val publicChannel = false - val pc = d.privateChannels(u.shortChannelId) - val desc = getDesc(u, pc) - if (StaleChannels.isStale(u)) { - log.debug("ignoring {} (stale)", u) - sendDecision(origins, GossipDecision.Stale(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { - log.debug("ignoring {} (already know same or newer)", u) - sendDecision(origins, GossipDecision.Duplicate(u)) - d - } else if (!Announcements.checkSig(u, desc.a)) { - log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) - sendDecision(origins, GossipDecision.InvalidSignature(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { - d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + case None if d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId) => + // channel is currently being validated + if (d.stash.updates.contains(u)) { + log.debug("ignoring {} (already stashed)", u) + val origins1 = d.stash.updates(u) ++ origins + d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins1))) } else { - d.graph.removeEdge(desc) + log.debug("stashing {}", u) + d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) } - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) - } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) - } - } else if (db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u)) { - // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case - // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, - // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, - // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) - // about that channel. We can ignore this update since we will receive it again - log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel") - sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) - db.removeFromPruned(u.shortChannelId) - // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not - // when we're sending updates to ourselves - origins head match { - case RemoteGossip(peerConnection, remoteNodeId) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty) - d.sync.get(remoteNodeId) match { - case Some(sync) if sync.started => - // we already have a pending request to that node, let's add this channel to the list and we'll get it later - // TODO: we only request channels with old style channel_query - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) - case _ => - // otherwise we send the query right away - peerConnection ! query - d.copy(sync = d.sync + (remoteNodeId -> Syncing(remainingQueries = Nil, totalQueries = 1))) - } - case _ => - // we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case). - // or we don't have a peerConnection to send our query to. - // anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it - d - } - } else { - log.debug("ignoring announcement {} (unknown channel)", u) - sendDecision(origins, GossipDecision.NoRelatedChannel(u)) - d + case None if db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u) => + // only public channels are pruned + val realShortChannelId = RealShortChannelId(u.shortChannelId.toLong) + // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case + // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, + // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, + // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) + // about that channel. We can ignore this update since we will receive it again + log.info(s"channel shortChannelId=$realShortChannelId is back from the dead! requesting announcements about this channel") + sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) + db.removeFromPruned(realShortChannelId) + // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not + // when we're sending updates to ourselves + origins.head match { + case RemoteGossip(peerConnection, remoteNodeId) => + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(realShortChannelId)), TlvStream.empty) + d.sync.get(remoteNodeId) match { + case Some(sync) if sync.started => + // we already have a pending request to that node, let's add this channel to the list and we'll get it later + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) + case _ => + // otherwise we send the query right away + peerConnection ! query + d.copy(sync = d.sync + (remoteNodeId -> Syncing(remainingQueries = Nil, totalQueries = 1))) + } + case _ => + // we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case). + // or we don't have a peerConnection to send our query to. + // anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it + d + } + case None => + log.debug("ignoring announcement {} (unknown channel)", u) + sendDecision(origins, GossipDecision.NoRelatedChannel(u)) + d } } - def handleLocalChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, localNodeId: PublicKey, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + /** + * We will receive this event before [[LocalChannelUpdate]] or [[ChannelUpdate]] + */ + def handleShortChannelIdAssigned(d: Data, localNodeId: PublicKey, scia: ShortChannelIdAssigned)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - d.channels.get(lcu.shortChannelId) match { + // NB: we don't map remote aliases because they are decided by our peer and could overlap with ours + val mappings = scia.shortIds.real.toOption match { + case Some(realScid) => Map(realScid.toLong -> scia.channelId, scia.shortIds.localAlias.toLong -> scia.channelId) + case None => Map(scia.shortIds.localAlias.toLong -> scia.channelId) + } + log.debug("handleShortChannelIdAssigned scia={} mappings={}", scia, mappings) + val d1 = d.copy(scid2PrivateChannels = d.scid2PrivateChannels ++ mappings) + d1.resolve(scia.channelId, scia.shortIds.real.toOption) match { case Some(_) => - // channel has already been announced and router knows about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + // channel is known, nothing more to do + d1 case None => + // this is a local channel that hasn't yet been announced (maybe it is a private channel or maybe it is a public + // channel that doesn't yet have 6 confirmations), we create a corresponding private channel + val pc = PrivateChannel(scia.channelId, scia.shortIds, localNodeId, scia.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)) + log.debug("adding unannounced local channel to remote={} channelId={} localAlias={}", scia.remoteNodeId, scia.channelId, scia.shortIds.localAlias) + d1.copy(privateChannels = d1.privateChannels + (scia.channelId -> pc)) + } + } + + def handleLocalChannelUpdate(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { + implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors + import nodeParams.db.{network => db} + log.debug("handleLocalChannelUpdate lcu={}", lcu) + d.resolve(lcu.channelId, lcu.shortIds.real.toOption) match { + case Some(publicChannel: PublicChannel) => + // this a known public channel, we can process the channel_update + log.debug("this is a known public channel, processing channel_update publicChannel={}", publicChannel) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) + case Some(privateChannel: PrivateChannel) => lcu.channelAnnouncement_opt match { - case Some(c) if d.awaiting.contains(c) => - // channel is currently being verified, we can process the channel_update right away (it will be stashed) - handleChannelUpdate(d, db, routerConf, Left(lcu)) - case Some(c) => - // channel wasn't announced but here is the announcement, we will process it *before* the channel_update - watcher ! ValidateRequest(ctx.self, c) - val d1 = d.copy(awaiting = d.awaiting + (c -> Nil)) // no origin + case Some(ann) => + log.debug("private channel graduating to public privateChannel={}", privateChannel) + // channel is graduating from private to public + // since this is a local channel, we can trust the announcement, no need to go through the full + // verification process and make calls to bitcoin core + val fundingTxId = lcu.commitments match { + case commitments: Commitments => commitments.commitInput.outPoint.txid + case _ => ByteVector32.Zeroes + } + val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity, Some(privateChannel)) // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) - db.removeFromPruned(c.shortChannelId) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) - case None if d.privateChannels.contains(lcu.shortChannelId) => - // channel isn't announced but we already know about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + db.removeFromPruned(ann.shortChannelId) + log.debug("processing channel_update") + handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) case None => - // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) - // let's create a corresponding private channel and process the channel_update - log.debug("adding unannounced local channel to remote={} shortChannelId={}", lcu.remoteNodeId, lcu.shortChannelId) - val pc = PrivateChannel(localNodeId, lcu.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)).updateBalances(lcu.commitments) - val d1 = d.copy(privateChannels = d.privateChannels + (lcu.shortChannelId -> pc)) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) + log.debug("this is a known private channel, processing channel_update privateChannel={}", privateChannel) + // this a known private channel, we update the short ids (we now may have the remote_alias) and the balances + val pc1 = privateChannel.copy(shortIds = lcu.shortIds).updateBalances(lcu.commitments) + val d1 = d.copy(privateChannels = d.privateChannels + (privateChannel.channelId -> pc1)) + // then we can process the channel_update + handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) } + case None => + // should never happen, we log a warning and handle the update, it will be rejected since there is no related channel + log.warning("unrecognized local chanel update for channelId={} localAlias={}", lcu.channelId, lcu.shortIds.localAlias) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) } } def handleLocalChannelDown(d: Data, localNodeId: PublicKey, lcd: LocalChannelDown)(implicit log: LoggingAdapter): Data = { - import lcd.{channelId, remoteNodeId, shortChannelId} + import lcd.{channelId, remoteNodeId} + log.debug("handleLocalChannelDown lcd={}", lcd) + val scid2PrivateChannels1 = d.scid2PrivateChannels - lcd.shortIds.localAlias.toLong -- lcd.shortIds.real.toOption.map(_.toLong) // a local channel has permanently gone down - if (d.channels.contains(shortChannelId)) { + if (lcd.shortIds.real.toOption.exists(d.channels.contains)) { // the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel // so let's not do anything here - d - } else if (d.privateChannels.contains(shortChannelId)) { + d.copy(scid2PrivateChannels = scid2PrivateChannels1) + } else if (d.privateChannels.contains(lcd.channelId)) { // the channel was private or public-but-not-yet-announced, let's do the clean up - log.debug("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) - val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) - val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId) + val localAlias = d.privateChannels(channelId).shortIds.localAlias + log.info("removing private local channel and channel_update for channelId={} localAlias={}", channelId, localAlias) // we remove the corresponding updates from the graph - val graph1 = d.graph - .removeEdge(desc1) - .removeEdge(desc2) + val graphWithBalances1 = d.graphWithBalances + .removeEdge(ChannelDesc(localAlias, localNodeId, remoteNodeId)) + .removeEdge(ChannelDesc(localAlias, remoteNodeId, localNodeId)) // and we remove the channel and channel_update from our state - d.copy(privateChannels = d.privateChannels - shortChannelId, graph = graph1) + d.copy(privateChannels = d.privateChannels - channelId, scid2PrivateChannels = scid2PrivateChannels1, graphWithBalances = graphWithBalances1) } else { d } } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val desc = ChannelDesc(e.shortChannelId, e.commitments.localNodeId, e.commitments.remoteNodeId) - val (publicChannels1, graph1) = d.channels.get(e.shortChannelId) match { + val (publicChannels1, graphWithBalances1) = e.shortIds.real.toOption.flatMap(d.channels.get) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) - (d.channels + (e.shortChannelId -> pc1), graph1) + val graphWithBalances1 = update_opt.map(u => d.graphWithBalances.addEdge(GraphEdge(u, pc1))).getOrElse(d.graphWithBalances) + (d.channels + (pc.ann.shortChannelId -> pc1), graphWithBalances1) case None => - (d.channels, d.graph) + (d.channels, d.graphWithBalances) } - val (privateChannels1, graph2) = d.privateChannels.get(e.shortChannelId) match { + val (privateChannels1, graphWithBalances2) = d.privateChannels.get(e.channelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("private channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph2 = update_opt.map(u => graph1.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(graph1) - (d.privateChannels + (e.shortChannelId -> pc1), graph2) + val graphWithBalances2 = update_opt.map(u => graphWithBalances1.addEdge(GraphEdge(u, pc1))).getOrElse(graphWithBalances1) + (d.privateChannels + (e.channelId -> pc1), graphWithBalances2) case None => - (d.privateChannels, graph1) + (d.privateChannels, graphWithBalances1) } - d.copy(channels = publicChannels1, privateChannels = privateChannels1, graph = graph2) + d.copy(channels = publicChannels1, privateChannels = privateChannels1, graphWithBalances = graphWithBalances2) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 7fa28b1118..0f27c104e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -231,12 +231,13 @@ object Socks5ProxyParams { ) - def proxyAddress(socketAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = - NodeAddress.fromParts(socketAddress.getHostString, socketAddress.getPort).toOption collect { - case _: IPv4 if proxyParams.useForIPv4 => proxyParams.address - case _: IPv6 if proxyParams.useForIPv6 => proxyParams.address - case _: Tor2 if proxyParams.useForTor => proxyParams.address - case _: Tor3 if proxyParams.useForTor => proxyParams.address + def proxyAddress(address: NodeAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = + address match { + case _: IPv4 if proxyParams.useForIPv4 => Some(proxyParams.address) + case _: IPv6 if proxyParams.useForIPv6 => Some(proxyParams.address) + case _: Tor2 if proxyParams.useForTor => Some(proxyParams.address) + case _: Tor3 if proxyParams.useForTor => Some(proxyParams.address) + case _ => None } def proxyCredentials(proxyParams: Socks5ProxyParams): Option[Socks5Connection.Credentials] = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index c6f1584ded..b775280649 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -16,20 +16,19 @@ package fr.acinq.eclair.tor -import java.nio.file.attribute.PosixFilePermissions -import java.nio.file.{Files, Path, Paths} -import java.util - import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString -import fr.acinq.eclair.tor.TorProtocolHandler.{Authentication, OnionServiceVersion} -import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor2, Tor3} -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec +import fr.acinq.eclair.tor.TorProtocolHandler.Authentication +import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor3} import scodec.bits.Bases.Alphabets import scodec.bits.ByteVector +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{Files, Path, Paths} +import java.util +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import scala.concurrent.Promise import scala.util.Try @@ -40,15 +39,13 @@ case class TorException(private val msg: String) extends RuntimeException(s"Tor * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * - * @param onionServiceVersion v2 or v3 * @param authentication Tor controller auth mechanism (password or safecookie) * @param privateKeyPath path to a file that contains a Tor private key * @param virtualPort port for the public hidden service (typically 9735) * @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty * @param onionAdded a Promise to track creation of the endpoint */ -class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, - authentication: Authentication, +class TorProtocolHandler(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String], @@ -74,8 +71,8 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, val methods: String = res.getOrElse("METHODS", throw TorException("auth methods not found")) val torVersion = unquote(res.getOrElse("Tor", throw TorException("version not found"))) log.info(s"Tor version $torVersion") - if (!OnionServiceVersion.isCompatible(onionServiceVersion, torVersion)) { - throw TorException(s"version $torVersion does not support onion service $onionServiceVersion") + if (!isCompatible(torVersion)) { + throw TorException(s"unsupported Tor version: $torVersion") } if (!Authentication.isCompatible(authentication, methods)) { throw TorException(s"cannot use authentication '$authentication', supported methods are '$methods'") @@ -116,10 +113,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) - address = Some(onionServiceVersion match { - case V2 => Tor2(serviceId, virtualPort) - case V3 => Tor3(serviceId, virtualPort) - }) + address = Some(Tor3(serviceId, virtualPort)) onionAdded.foreach(_.success(address.get)) log.debug("Onion address: {}", address.get) } @@ -151,10 +145,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, if (privateKeyPath.toFile.exists()) { readString(privateKeyPath) } else { - onionServiceVersion match { - case V2 => "NEW:RSA1024" - case V3 => "NEW:ED25519-V3" - } + "NEW:ED25519-V3" } } @@ -190,48 +181,30 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, } object TorProtocolHandler { - def props(version: OnionServiceVersion, - authentication: Authentication, + def props(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String] = Seq(), onionAdded: Option[Promise[NodeAddress]] = None ): Props = - Props(new TorProtocolHandler(version, authentication, privateKeyPath, virtualPort, targets, onionAdded)) + Props(new TorProtocolHandler(authentication, privateKeyPath, virtualPort, targets, onionAdded)) // those are defined in the spec private val ServerKey = ByteVector.view("Tor safe cookie authentication server-to-controller hash".getBytes()) private val ClientKey = ByteVector.view("Tor safe cookie authentication controller-to-server hash".getBytes()) - // @formatter:off - sealed trait OnionServiceVersion - case object V2 extends OnionServiceVersion - case object V3 extends OnionServiceVersion - // @formatter:on - - object OnionServiceVersion { - def apply(s: String): OnionServiceVersion = s match { - case "v2" | "V2" => V2 - case "v3" | "V3" => V3 - case _ => throw TorException(s"unknown protocol version `$s`") - } - - def isCompatible(onionServiceVersion: OnionServiceVersion, torVersion: String): Boolean = - onionServiceVersion match { - case V2 => true - case V3 => torVersion - .split("\\.") - .map(_.split('-').head) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) - .map(d => Try(d.toInt).getOrElse(0)) - .zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6 - .foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left - case (Some(res), _) => Some(res) // we stop the comparison as soon as there is a difference - case (None, (v, vref)) => if (v > vref) Some(true) else if (v < vref) Some(false) else None - } - .getOrElse(true) // if version == 0.3.3.6 then result will be None + private[tor] def isCompatible(torVersion: String): Boolean = + torVersion + .split("\\.") + .map(_.split('-').head) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) + .map(d => Try(d.toInt).getOrElse(0)) + .zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6 + .foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left + case (Some(res), _) => Some(res) // we stop the comparison as soon as there is a difference + case (None, (v, vref)) => if (v > vref) Some(true) else if (v < vref) Some(false) else None } - } + .getOrElse(true) // if version == 0.3.3.6 then result will be None // @formatter:off sealed trait Authentication diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 414897a7cd..20dd718266 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 9cecd2c059..fdccd4c0a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -16,9 +16,12 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.SigHash._ +import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_TYPE_FLAG, SEQUENCE_LOCKTIME_MASK} +import fr.acinq.bitcoin.Script.LockTimeThreshold import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -69,7 +72,7 @@ object Scripts { def encodeNumber(n: Long): ScriptElt = n match { case 0 => OP_0 case -1 => OP_1NEGATE - case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt) + case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt).get case _ => OP_PUSHDATA(Script.encodeNumber(n)) } @@ -94,11 +97,11 @@ object Scripts { } private def sequenceToBlockHeight(sequence: Long): Long = { - if ((sequence & TxIn.SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) { + if ((sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) { 0 } else { - require((sequence & TxIn.SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times") - sequence & TxIn.SEQUENCE_LOCKTIME_MASK + require((sequence & SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times") + sequence & SEQUENCE_LOCKTIME_MASK } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index d7e6f13ace..3256c731f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -16,10 +16,12 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160} -import fr.acinq.bitcoin.Script._ +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, ripemd160} +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.CommitmentOutput._ @@ -273,10 +275,10 @@ object Transactions { * down to Satoshi. */ def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { - // The funder pays the on-chain fee by deducing it from its main output. + // The channel initiator pays the on-chain fee by deducing it from its main output. val txFee = commitTxFeeMsat(dustLimit, spec, commitmentFormat) - // When using anchor outputs, the funder pays for *both* anchors all the time, even if only one anchor is present. - // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the funder's main output. + // When using anchor outputs, the channel initiator pays for *both* anchors all the time, even if only one anchor is present. + // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output. val anchorsCost = commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 @@ -288,14 +290,14 @@ object Transactions { /** * @param commitTxNumber commit tx number - * @param isFunder true if local node is funder + * @param isInitiator true if local node initiated the channel open * @param localPaymentBasePoint local payment base point * @param remotePaymentBasePoint remote payment base point * @return the obscured tx number as defined in BOLT #3 (a 48 bits integer) */ - def obscuredCommitTxNumber(commitTxNumber: Long, isFunder: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { + def obscuredCommitTxNumber(commitTxNumber: Long, isInitiator: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { // from BOLT 3: SHA256(payment-basepoint from open_channel || payment-basepoint from accept_channel) - val h = if (isFunder) { + val h = if (isInitiator) { Crypto.sha256(localPaymentBasePoint.value ++ remotePaymentBasePoint.value) } else { Crypto.sha256(remotePaymentBasePoint.value ++ localPaymentBasePoint.value) @@ -306,14 +308,14 @@ object Transactions { /** * @param commitTx commit tx - * @param isFunder true if local node is funder + * @param isInitiator true if local node initiated the channel open * @param localPaymentBasePoint local payment base point * @param remotePaymentBasePoint remote payment base point * @return the actual commit tx number that was blinded and stored in locktime and sequence fields */ - def getCommitTxNumber(commitTx: Transaction, isFunder: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { + def getCommitTxNumber(commitTx: Transaction, isInitiator: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { require(commitTx.txIn.size == 1, "commitment tx should have 1 input") - val blind = obscuredCommitTxNumber(0, isFunder, localPaymentBasePoint, remotePaymentBasePoint) + val blind = obscuredCommitTxNumber(0, isInitiator, localPaymentBasePoint, remotePaymentBasePoint) val obscured = decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) obscured ^ blind } @@ -361,7 +363,7 @@ object Transactions { } } - def makeCommitTxOutputs(localIsFunder: Boolean, + def makeCommitTxOutputs(localIsInitiator: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, @@ -387,7 +389,7 @@ object Transactions { val hasHtlcs = outputs.nonEmpty - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsInitiator) { (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat)) @@ -431,9 +433,9 @@ object Transactions { commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, - localIsFunder: Boolean, + localIsInitiator: Boolean, outputs: CommitmentOutputs): CommitTx = { - val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) + val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsInitiator, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, lockTime) = encodeTxNumber(txNumber) val tx = Transaction( @@ -781,10 +783,10 @@ object Transactions { } } - def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { + def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localIsInitiator: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsInitiator) { (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala index 6a8fb44bfb..d0b116072b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.internal.channel -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0 import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1 import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2 @@ -65,10 +65,10 @@ object ChannelCodecs extends Logging { * * More info here: https://github.com/scodec/scodec/issues/122 */ - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(byte) - .typecase(3, ChannelCodecs3.stateDataCodec) - .typecase(2, ChannelCodecs2.stateDataCodec.decodeOnly) - .typecase(1, ChannelCodecs1.stateDataCodec.decodeOnly) - .typecase(0, ChannelCodecs0.stateDataCodec.decodeOnly) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(byte) + .typecase(3, ChannelCodecs3.channelDataCodec) + .typecase(2, ChannelCodecs2.channelDataCodec.decodeOnly) + .typecase(1, ChannelCodecs1.channelDataCodec.decodeOnly) + .typecase(0, ChannelCodecs0.channelDataCodec.decodeOnly) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 602662aa2f..c67fe3573d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel.version0 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -26,10 +26,11 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, TimestampSecond} +import fr.acinq.eclair.{BlockHeight, Alias, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ +import shapeless.{::, HNil} import java.util.UUID @@ -42,14 +43,16 @@ private[channel] object ChannelCodecs0 { private[version0] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath].decodeOnly + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath].decodeOnly val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey].decodeOnly + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = discriminatorWithDefault[ChannelTypes0.ChannelVersion]( discriminator = discriminated[ChannelTypes0.ChannelVersion].by(byte) @@ -65,11 +68,11 @@ private[channel] object ChannelCodecs0 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool) :: + ("isInitiator" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams].decodeOnly @@ -78,7 +81,7 @@ private[channel] object ChannelCodecs0 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -272,7 +275,7 @@ private[channel] object ChannelCodecs0 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: @@ -346,30 +349,33 @@ private[channel] object ChannelCodecs0 { ("signature" | bytes64) :: ("tlvStream" | provide(TlvStream.empty[FundingSignedTlv]))).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | provide(TlvStream.empty[FundingLockedTlv]))).as[FundingLocked] + ("tlvStream" | provide(TlvStream.empty[ChannelReadyTlv]))).as[ChannelReady] // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields - val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_02_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED].decodeOnly + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | channelReadyCodec)).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: @@ -377,33 +383,39 @@ private[channel] object ChannelCodecs0 { ("tlvStream" | provide(TlvStream.empty[ShutdownTlv]))).as[Shutdown] // this is a decode-only codec compatible with versions 9afb26e and below - val DATA_NORMAL_COMPAT_03_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_03_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(noUnknownFieldsChannelAnnouncementSizeCodec, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_10_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(uint16, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_04_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN].decodeOnly - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_05_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec) :: @@ -411,7 +423,7 @@ private[channel] object ChannelCodecs0 { ("bestUnpublishedClosingTx_opt" | optional(bool, closingTxCodec))).as[DATA_NEGOTIATING].decodeOnly // this is a decode-only codec compatible with versions 818199e and below, with placeholders for new fields - val DATA_CLOSING_COMPAT_06_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_06_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: @@ -423,7 +435,7 @@ private[channel] object ChannelCodecs0 { ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING].decodeOnly - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_09_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -443,22 +455,22 @@ private[channel] object ChannelCodecs0 { ("myCurrentPerCommitmentPoint" | publicKey) :: ("tlvStream" | provide(TlvStream.empty[ChannelReestablishTlv]))).as[ChannelReestablish] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT].decodeOnly } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) - .typecase(0x10, Codecs.DATA_NORMAL_Codec) - .typecase(0x09, Codecs.DATA_CLOSING_Codec) - .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x03, Codecs.DATA_NORMAL_COMPAT_03_Codec) - .typecase(0x04, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x05, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x06, Codecs.DATA_CLOSING_COMPAT_06_Codec) - .typecase(0x07, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x10, Codecs.DATA_NORMAL_10_Codec) + .typecase(0x09, Codecs.DATA_CLOSING_09_Codec) + .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_02_Codec) + .typecase(0x03, Codecs.DATA_NORMAL_03_Codec) + .typecase(0x04, Codecs.DATA_SHUTDOWN_04_Codec) + .typecase(0x05, Codecs.DATA_NEGOTIATING_05_Codec) + .typecase(0x06, Codecs.DATA_CLOSING_06_Codec) + .typecase(0x07, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index f136208bb3..16b15c7bc3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -17,13 +17,13 @@ package fr.acinq.eclair.wire.internal.channel.version0 import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, Feature, Features, channel} +import fr.acinq.eclair.{BlockHeight, ChannelTypeFeature, Features, PermanentChannelFeature, channel} import scodec.bits.BitVector private[channel] object ChannelTypes0 { @@ -181,7 +181,7 @@ private[channel] object ChannelTypes0 { case class Commitments(channelVersion: ChannelVersion, localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: Byte, + channelFlags: ChannelFlags, localCommit: LocalCommit, remoteCommit: RemoteCommit, localChanges: LocalChanges, remoteChanges: RemoteChanges, localNextHtlcId: Long, remoteNextHtlcId: Long, @@ -196,8 +196,8 @@ private[channel] object ChannelTypes0 { ChannelConfig() } val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215) - val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty - val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) { + val baseChannelFeatures: Set[PermanentChannelFeature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty + val commitmentFeatures: Set[ChannelTypeFeature] = if (channelVersion.hasAnchorOutputs) { Set(Features.StaticRemoteKey, Features.AnchorOutputs) } else if (channelVersion.hasStaticRemotekey) { Set(Features.StaticRemoteKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 9658868cc5..97be45a049 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.wire.internal.channel.version1 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -31,19 +31,22 @@ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs1 { private[version1] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] @@ -52,11 +55,11 @@ private[channel] object ChannelCodecs1 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] @@ -65,7 +68,7 @@ private[channel] object ChannelCodecs1 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -188,7 +191,7 @@ private[channel] object ChannelCodecs1 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: @@ -229,42 +232,48 @@ private[channel] object ChannelCodecs1 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) :: ("spent" | spentMapCodec)).as[ChannelTypes0.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_21_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_22_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_23_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_24_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_25_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -276,18 +285,18 @@ private[channel] object ChannelCodecs1 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) - .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x21, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x22, Codecs.DATA_NORMAL_Codec) - .typecase(0x23, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x24, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x25, Codecs.DATA_CLOSING_Codec) - .typecase(0x26, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec) + .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_21_Codec) + .typecase(0x22, Codecs.DATA_NORMAL_22_Codec) + .typecase(0x23, Codecs.DATA_SHUTDOWN_23_Codec) + .typecase(0x24, Codecs.DATA_NEGOTIATING_24_Codec) + .typecase(0x25, Codecs.DATA_CLOSING_25_Codec) + .typecase(0x26, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 25e8f7e831..fb132d33ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.wire.internal.channel.version2 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -31,19 +31,22 @@ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs2 { private[version2] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] @@ -52,11 +55,11 @@ private[channel] object ChannelCodecs2 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] @@ -65,7 +68,7 @@ private[channel] object ChannelCodecs2 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -223,7 +226,7 @@ private[channel] object ChannelCodecs2 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: @@ -264,42 +267,48 @@ private[channel] object ChannelCodecs2 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: ("spent" | spentMapCodec)).as[RevokedCommitPublished] - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -311,18 +320,18 @@ private[channel] object ChannelCodecs2 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) + .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) + .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) + .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) + .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) + .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 0dde1cd9b2..822e224116 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -25,23 +25,27 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, Alias, PermanentChannelFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs3 { private[version3] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)).xmap( + { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, + { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } + ) val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { @@ -62,7 +66,7 @@ private[channel] object ChannelCodecs3 { /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( - (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet), // we make no difference between mandatory/optional, both are considered activated + (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention ) @@ -71,20 +75,20 @@ private[channel] object ChannelCodecs3 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] - val remoteParamsCodec: Codec[RemoteParams] = ( + def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[RemoteParams] = ( ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -265,8 +269,8 @@ private[channel] object ChannelCodecs3 { ("channelConfig" | channelConfigCodec) :: (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => ("localParams" | localParamsCodec(channelFeatures)) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("remoteParams" | remoteParamsCodec(channelFeatures)) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: @@ -311,59 +315,82 @@ private[channel] object ChannelCodecs3 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: ("spent" | spentMapCodec)).as[RevokedCommitPublished] - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | commitmentsCodec) :: + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly + + val DATA_WAIT_FOR_CHANNEL_READY_0a_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortIds" | shortids) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).as[DATA_WAIT_FOR_CHANNEL_READY] - val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_07_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly + + val DATA_NORMAL_09_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | commitmentsCodec) :: + ("shortids" | shortids) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_NORMAL] - val DATA_SHUTDOWN_COMPAT_03_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_08_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) @@ -376,21 +403,23 @@ private[channel] object ChannelCodecs3 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) - .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x07, Codecs.DATA_NORMAL_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_COMPAT_03_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_COMPAT_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_0a_Codec) + .typecase(0x09, Codecs.DATA_NORMAL_09_Codec) + .typecase(0x08, Codecs.DATA_SHUTDOWN_08_Codec) + .typecase(0x07, Codecs.DATA_NORMAL_07_Codec) + .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) + .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) + .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) + .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) + .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 56e362350b..27a32898f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -16,11 +16,12 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} +import fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream -import fr.acinq.eclair.{FeatureSupport, Features, UInt64} +import fr.acinq.eclair.{Alias, FeatureSupport, Features, ShortChannelId, UInt64} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs._ @@ -29,20 +30,24 @@ sealed trait OpenChannelTlv extends Tlv sealed trait AcceptChannelTlv extends Tlv +sealed trait OpenDualFundedChannelTlv extends Tlv + +sealed trait AcceptDualFundedChannelTlv extends Tlv + object ChannelTlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ - case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv { + case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv { val isEmpty: Boolean = script.isEmpty } val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScriptTlv] /** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */ - case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv + case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap( - b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b))), + b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())), tlv => Features(tlv.channelType.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector ) @@ -69,6 +74,28 @@ object AcceptChannelTlv { ) } +object OpenDualFundedChannelTlv { + + import ChannelTlv._ + + val openTlvCodec: Codec[TlvStream[OpenDualFundedChannelTlv]] = tlvStream(discriminated[OpenDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + +object AcceptDualFundedChannelTlv { + + import ChannelTlv._ + + val acceptTlvCodec: Codec[TlvStream[AcceptDualFundedChannelTlv]] = tlvStream(discriminated[AcceptDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { @@ -81,10 +108,17 @@ object FundingSignedTlv { val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)) } -sealed trait FundingLockedTlv extends Tlv +sealed trait ChannelReadyTlv extends Tlv + +object ChannelReadyTlv { -object FundingLockedTlv { - val fundingLockedTlvCodec: Codec[TlvStream[FundingLockedTlv]] = tlvStream(discriminated[FundingLockedTlv].by(varint)) + case class ShortChannelIdTlv(alias: Alias) extends ChannelReadyTlv + + val channelAliasTlvCodec: Codec[ShortChannelIdTlv] = variableSizeBytesLong(varintoverflow, "alias" | alias).as[ShortChannelIdTlv] + + val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) + .typecase(UInt64(1), channelAliasTlvCodec) + ) } sealed trait ChannelReestablishTlv extends Tlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 344e0e0e70..314fbaf4bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -16,11 +16,12 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.{ChannelFlags, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -109,6 +110,8 @@ object CommonCodecs { val listofsignatures: Codec[List[ByteVector64]] = listOfN(uint16, bytes64) + val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] + val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) @@ -127,7 +130,22 @@ object CommonCodecs { // number of bytes we can just skip to the next field val listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress)) - val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong) + val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => UnspecifiedShortChannelId(l), s => s.toLong) + + val realshortchannelid: Codec[RealShortChannelId] = shortchannelid.narrow[RealShortChannelId](scid => Attempt.successful(RealShortChannelId(scid.toLong)), scid => scid) + + val alias: Codec[Alias] = shortchannelid.narrow[Alias](scid => Attempt.successful(Alias(scid.toLong)), scid => scid) + + val realShortChannelIdStatus: Codec[RealScidStatus] = discriminated[RealScidStatus].by(uint8) + .typecase(0, provide(RealScidStatus.Unknown)) + .typecase(1, realshortchannelid.as[RealScidStatus.Temporary]) + .typecase(2, realshortchannelid.as[RealScidStatus.Final]) + + val shortids: Codec[ShortIds] = ( + ("real" | realShortChannelIdStatus) :: + ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: // forward-compatible with listOfN(uint16, aliashortchannelid) in case we want to store a list of local aliases later + ("remoteAlias_opt" | optional(bool8, alias)) + ).as[ShortIds] val privateKey: Codec[PrivateKey] = Codec[PrivateKey]( (priv: PrivateKey) => bytes(32).encode(priv.value), @@ -141,6 +159,8 @@ object CommonCodecs { val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) + val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) + def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(size, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 15b72b5231..1e9612c22a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec @@ -139,9 +139,11 @@ object FailureMessageCodecs { /** * An onion-encrypted failure from an intermediate node: + * {{{ * +----------------+----------------------------------+-----------------+----------------------+-----+ * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | * +----------------+----------------------------------+-----------------+----------------------+-----+ + * }}} * with failure message length + pad length = 256 */ def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala new file mode 100644 index 0000000000..5776eaa5ac --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.scalacompat.Satoshi +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.protocol.CommonCodecs.{varint, varintoverflow} +import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvStream, tsatoshi} +import scodec.Codec +import scodec.codecs.{discriminated, variableSizeBytesLong} + +/** + * Created by t-bast on 08/04/2022. + */ + +sealed trait TxAddInputTlv extends Tlv + +object TxAddInputTlv { + val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)) +} + +sealed trait TxAddOutputTlv extends Tlv + +object TxAddOutputTlv { + val txAddOutputTlvCodec: Codec[TlvStream[TxAddOutputTlv]] = tlvStream(discriminated[TxAddOutputTlv].by(varint)) +} + +sealed trait TxRemoveInputTlv extends Tlv + +object TxRemoveInputTlv { + val txRemoveInputTlvCodec: Codec[TlvStream[TxRemoveInputTlv]] = tlvStream(discriminated[TxRemoveInputTlv].by(varint)) +} + +sealed trait TxRemoveOutputTlv extends Tlv + +object TxRemoveOutputTlv { + val txRemoveOutputTlvCodec: Codec[TlvStream[TxRemoveOutputTlv]] = tlvStream(discriminated[TxRemoveOutputTlv].by(varint)) +} + +sealed trait TxCompleteTlv extends Tlv + +object TxCompleteTlv { + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) +} + +sealed trait TxSignaturesTlv extends Tlv + +object TxSignaturesTlv { + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)) +} + +sealed trait TxInitRbfTlv extends Tlv + +sealed trait TxAckRbfTlv extends Tlv + +object TxRbfTlv { + /** Amount that the peer will contribute to the transaction's shared output. */ + case class SharedOutputContributionTlv(amount: Satoshi) extends TxInitRbfTlv with TxAckRbfTlv +} + +object TxInitRbfTlv { + + import TxRbfTlv._ + + val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +object TxAckRbfTlv { + + import TxRbfTlv._ + + val txAckRbfTlvCodec: Codec[TlvStream[TxAckRbfTlv]] = tlvStream(discriminated[TxAckRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +sealed trait TxAbortTlv extends Tlv + +object TxAbortTlv { + val txAbortTlvCodec: Codec[TlvStream[TxAbortTlv]] = tlvStream(discriminated[TxAbortTlv].by(varint)) +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 3299b44f80..4ffa28885a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.ScriptWitness import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{Features, KamonExt} +import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -29,18 +30,20 @@ import shapeless._ */ object LightningMessageCodecs { - val featuresCodec: Codec[Features] = varsizebinarydata.xmap[Features]( + val featuresCodec: Codec[Features[Feature]] = varsizebinarydata.xmap[Features[Feature]]( { bytes => Features(bytes) }, { features => features.toByteVector } ) + val initFeaturesCodec: Codec[Features[InitFeature]] = featuresCodec.xmap[Features[InitFeature]](_.initFeatures(), _.unscoped()) + /** For historical reasons, features are divided into two feature bitmasks. We only send from the second one, but we allow receiving in both. */ - val combinedFeaturesCodec: Codec[Features] = ( + val combinedFeaturesCodec: Codec[Features[InitFeature]] = ( ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features]( + ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features[InitFeature]]( { case (gf, lf) => val length = gf.length.max(lf.length) - Features(gf.padLeft(length) | lf.padLeft(length)) + Features(gf.padLeft(length) | lf.padLeft(length)).initFeatures() }, { features => (ByteVector.empty, features.toByteVector) }) @@ -79,7 +82,7 @@ object LightningMessageCodecs { ("fundingSatoshis" | satoshi) :: ("pushMsat" | millisatoshi) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("feeratePerKw" | feeratePerKw) :: @@ -91,13 +94,34 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("tlvStream" | OpenChannelTlv.openTlvCodec)).as[OpenChannel] + val openDualFundedChannelCodec: Codec[OpenDualFundedChannel] = ( + ("chainHash" | bytes32) :: + ("temporaryChannelId" | bytes32) :: + ("fundingFeerate" | feeratePerKw) :: + ("commitmentFeerate" | feeratePerKw) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("lockTime" | uint32) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("channelFlags" | channelflags) :: + ("tlvStream" | OpenDualFundedChannelTlv.openTlvCodec)).as[OpenDualFundedChannel] + val acceptChannelCodec: Codec[AcceptChannel] = ( ("temporaryChannelId" | bytes32) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("minimumDepth" | uint32) :: @@ -111,6 +135,23 @@ object LightningMessageCodecs { ("firstPerCommitmentPoint" | publicKey) :: ("tlvStream" | AcceptChannelTlv.acceptTlvCodec)).as[AcceptChannel] + val acceptDualFundedChannelCodec: Codec[AcceptDualFundedChannel] = ( + ("temporaryChannelId" | bytes32) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("minimumDepth" | uint32) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("tlvStream" | AcceptDualFundedChannelTlv.acceptTlvCodec)).as[AcceptDualFundedChannel] + val fundingCreatedCodec: Codec[FundingCreated] = ( ("temporaryChannelId" | bytes32) :: ("fundingTxid" | bytes32) :: @@ -123,10 +164,64 @@ object LightningMessageCodecs { ("signature" | bytes64) :: ("tlvStream" | FundingSignedTlv.fundingSignedTlvCodec)).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] + ("tlvStream" | ChannelReadyTlv.channelReadyTlvCodec)).as[ChannelReady] + + val txAddInputCodec: Codec[TxAddInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("previousTx" | variableSizeBytes(uint16, txCodec)) :: + ("previousTxOutput" | uint32) :: + ("sequence" | uint32) :: + ("tlvStream" | TxAddInputTlv.txAddInputTlvCodec)).as[TxAddInput] + + val txAddOutputCodec: Codec[TxAddOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | variableSizeBytes(uint16, bytes)) :: + ("tlvStream" | TxAddOutputTlv.txAddOutputTlvCodec)).as[TxAddOutput] + + val txRemoveInputCodec: Codec[TxRemoveInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveInputTlv.txRemoveInputTlvCodec)).as[TxRemoveInput] + + val txRemoveOutputCodec: Codec[TxRemoveOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveOutputTlv.txRemoveOutputTlvCodec)).as[TxRemoveOutput] + + val txCompleteCodec: Codec[TxComplete] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxCompleteTlv.txCompleteTlvCodec)).as[TxComplete] + + private val witnessElementCodec: Codec[ByteVector] = variableSizeBytes(uint16, bytes) + private val witnessStackCodec: Codec[ScriptWitness] = listOfN(uint16, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(uint16, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) + + val txSignaturesCodec: Codec[TxSignatures] = ( + ("channelId" | bytes32) :: + ("txId" | sha256) :: + ("witnesses" | witnessesCodec) :: + ("tlvStream" | TxSignaturesTlv.txSignaturesTlvCodec)).as[TxSignatures] + + val txInitRbfCodec: Codec[TxInitRbf] = ( + ("channelId" | bytes32) :: + ("lockTime" | uint32) :: + ("feerate" | feeratePerKw) :: + ("tlvStream" | TxInitRbfTlv.txInitRbfTlvCodec)).as[TxInitRbf] + + val txAckRbfCodec: Codec[TxAckRbf] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxAckRbfTlv.txAckRbfTlvCodec)).as[TxAckRbf] + + val txAbortCodec: Codec[TxAbort] = ( + ("channelId" | bytes32) :: + ("data" | variableSizeBytes(uint16, bytes)) :: + ("tlvStream" | TxAbortTlv.txAbortTlvCodec)).as[TxAbort] val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: @@ -186,7 +281,7 @@ object LightningMessageCodecs { val announcementSignaturesCodec: Codec[AnnouncementSignatures] = ( ("channelId" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeSignature" | bytes64) :: ("bitcoinSignature" | bytes64) :: ("tlvStream" | AnnouncementSignaturesTlv.announcementSignaturesTlvCodec)).as[AnnouncementSignatures] @@ -194,7 +289,7 @@ object LightningMessageCodecs { val channelAnnouncementWitnessCodec = ("features" | featuresCodec) :: ("chainHash" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeId1" | publicKey) :: ("nodeId2" | publicKey) :: ("bitcoinKey1" | publicKey) :: @@ -273,10 +368,10 @@ object LightningMessageCodecs { .\(0) { case a@EncodedShortChannelIds(_, Nil) => a // empty list is always encoded with encoding type 'uncompressed' for compatibility with other implementations case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a - }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(realshortchannelid)).as[EncodedShortChannelIds]) .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a - }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(realshortchannelid))).as[EncodedShortChannelIds]) val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | bytes32) :: @@ -346,9 +441,20 @@ object LightningMessageCodecs { .typecase(33, acceptChannelCodec) .typecase(34, fundingCreatedCodec) .typecase(35, fundingSignedCodec) - .typecase(36, fundingLockedCodec) + .typecase(36, channelReadyCodec) .typecase(38, shutdownCodec) .typecase(39, closingSignedCodec) + .typecase(64, openDualFundedChannelCodec) + .typecase(65, acceptDualFundedChannelCodec) + .typecase(66, txAddInputCodec) + .typecase(67, txAddOutputCodec) + .typecase(68, txRemoveInputCodec) + .typecase(69, txRemoveOutputCodec) + .typecase(70, txCompleteCodec) + .typecase(71, txSignaturesCodec) + .typecase(72, txInitRbfCodec) + .typecase(73, txAckRbfCodec) + .typecase(74, txAbortCodec) .typecase(128, updateAddHtlcCodec) .typecase(130, updateFulfillHtlcCodec) .typecase(131, updateFailHtlcCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index ff88977696..7d8aa9f3e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -17,14 +17,17 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import com.google.common.net.InetAddresses +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelType -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} +import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv import scodec.bits.ByteVector -import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} +import java.net.{Inet4Address, Inet6Address, InetAddress} import java.nio.charset.StandardCharsets import scala.util.Try @@ -36,6 +39,7 @@ import scala.util.Try sealed trait LightningMessage extends Serializable sealed trait SetupMessage extends LightningMessage sealed trait ChannelMessage extends LightningMessage +sealed trait InteractiveTxMessage extends LightningMessage sealed trait HtlcMessage extends LightningMessage sealed trait RoutingMessage extends LightningMessage sealed trait AnnouncementMessage extends RoutingMessage // <- not in the spec @@ -47,7 +51,7 @@ sealed trait UpdateMessage extends HtlcMessage // <- not in the spec sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <- not in the spec // @formatter:on -case class Init(features: Features, tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { +case class Init(features: Features[InitFeature], tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address) } @@ -78,6 +82,47 @@ case class Ping(pongLength: Int, data: ByteVector, tlvStream: TlvStream[PingTlv] case class Pong(data: ByteVector, tlvStream: TlvStream[PongTlv] = TlvStream.empty) extends SetupMessage +case class TxAddInput(channelId: ByteVector32, + serialId: UInt64, + previousTx: Transaction, + previousTxOutput: Long, + sequence: Long, + tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAddOutput(channelId: ByteVector32, + serialId: UInt64, + amount: Satoshi, + pubkeyScript: ByteVector, + tlvStream: TlvStream[TxAddOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveInput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveOutput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxComplete(channelId: ByteVector32, + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxSignatures(channelId: ByteVector32, + txId: ByteVector32, + witnesses: Seq[ScriptWitness], + tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxInitRbf(channelId: ByteVector32, + lockTime: Long, + feerate: FeeratePerKw, + tlvStream: TlvStream[TxInitRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAckRbf(channelId: ByteVector32, + tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAbort(channelId: ByteVector32, + data: ByteVector, + tlvStream: TlvStream[TxAbortTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + case class ChannelReestablish(channelId: ByteVector32, nextLocalCommitmentNumber: Long, nextRemoteRevocationNumber: Long, @@ -102,7 +147,7 @@ case class OpenChannel(chainHash: ByteVector32, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, - channelFlags: Byte, + channelFlags: ChannelFlags, tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) @@ -127,6 +172,50 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) } +// NB: this message is named open_channel2 in the specification. +case class OpenDualFundedChannel(chainHash: ByteVector32, + temporaryChannelId: ByteVector32, + fundingFeerate: FeeratePerKw, + commitmentFeerate: FeeratePerKw, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + lockTime: Long, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + tlvStream: TlvStream[OpenDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + +// NB: this message is named accept_channel2 in the specification. +case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + minimumDepth: Long, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + tlvStream: TlvStream[AcceptDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxid: ByteVector32, fundingOutputIndex: Int, @@ -137,9 +226,11 @@ case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId -case class FundingLocked(channelId: ByteVector32, +case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[FundingLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) +} case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, @@ -191,7 +282,7 @@ case class UpdateFee(channelId: ByteVector32, tlvStream: TlvStream[UpdateFeeTlv] = TlvStream.empty) extends ChannelMessage with UpdateMessage with HasChannelId case class AnnouncementSignatures(channelId: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeSignature: ByteVector64, bitcoinSignature: ByteVector64, tlvStream: TlvStream[AnnouncementSignaturesTlv] = TlvStream.empty) extends RoutingMessage with HasChannelId @@ -200,9 +291,9 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, nodeSignature2: ByteVector64, bitcoinSignature1: ByteVector64, bitcoinSignature2: ByteVector64, - features: Features, + features: Features[Feature], chainHash: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, @@ -214,7 +305,7 @@ case class Color(r: Byte, g: Byte, b: Byte) { } // @formatter:off -sealed trait NodeAddress { def socketAddress: InetSocketAddress } +sealed trait NodeAddress { def host: String; def port: Int; override def toString: String = s"$host:$port" } sealed trait OnionAddress extends NodeAddress sealed trait IPAddress extends NodeAddress // @formatter:on @@ -232,7 +323,7 @@ object NodeAddress { host match { case _ if host.endsWith(".onion") && host.length == 22 => Tor2(host.dropRight(6), port) case _ if host.endsWith(".onion") && host.length == 62 => Tor3(host.dropRight(6), port) - case _ => IPAddress(InetAddress.getByName(host), port).get + case _ => IPAddress(InetAddress.getByName(host), port) } } @@ -248,22 +339,21 @@ object NodeAddress { } object IPAddress { - def apply(inetAddress: InetAddress, port: Int): Option[IPAddress] = inetAddress match { - case address: Inet4Address => Some(IPv4(address, port)) - case address: Inet6Address => Some(IPv6(address, port)) - case _ => None + def apply(inetAddress: InetAddress, port: Int): IPAddress = inetAddress match { + case address: Inet4Address => IPv4(address, port) + case address: Inet6Address => IPv6(address, port) } } // @formatter:off -case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv4, port) } -case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv6, port) } -case class Tor2(tor2: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) } -case class Tor3(tor3: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) } +case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv4) } +case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv6) } +case class Tor2(tor2: String, port: Int) extends OnionAddress { override def host: String = tor2 + ".onion" } +case class Tor3(tor3: String, port: Int) extends OnionAddress { override def host: String = tor3 + ".onion" } // @formatter:on case class NodeAnnouncement(signature: ByteVector64, - features: Features, + features: Features[Feature], timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, @@ -286,6 +376,8 @@ case class ChannelUpdate(signature: ByteVector64, def messageFlags: Byte = if (htlcMaximumMsat.isDefined) 1 else 0 def toStringShort: String = s"cltvExpiryDelta=$cltvExpiryDelta,feeBase=$feeBaseMsat,feeProportionalMillionths=$feeProportionalMillionths" + + def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBaseMsat, feeProportionalMillionths = feeProportionalMillionths) } object ChannelUpdate { @@ -305,7 +397,7 @@ object EncodingType { } // @formatter:on -case class EncodedShortChannelIds(encoding: EncodingType, array: List[ShortChannelId]) { +case class EncodedShortChannelIds(encoding: EncodingType, array: List[RealShortChannelId]) { /** custom toString because it can get huge in logs */ override def toString: String = s"EncodedShortChannelIds($encoding,${array.headOption.getOrElse("")}->${array.lastOption.getOrElse("")} size=${array.size})" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index 4c09d810f7..226c48c70a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -16,10 +16,13 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64 import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceErrorCodec, invoiceRequestCodec} import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} +import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream import scodec.bits.ByteVector /** Tlv types used inside the onion of an [[OnionMessage]]. */ @@ -40,6 +43,24 @@ object OnionMessagePayloadTlv { */ case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv + /** + * In order to pay a Bolt 12 offer, we must send an onion message to request an invoice corresponding to that offer. + * The creator of the offer will send us an invoice back through our blinded reply path. + */ + case class InvoiceRequest(request: OfferTypes.InvoiceRequest) extends OnionMessagePayloadTlv + + /** + * When receiving an invoice request, we must send an onion message back containing an invoice corresponding to the + * requested offer (if it was an offer we published). + */ + case class Invoice(invoice: Bolt12Invoice) extends OnionMessagePayloadTlv + + /** + * This message may be used when we receive an invalid invoice or invoice request. + * It contains information helping senders figure out why their message was invalid. + */ + case class InvoiceError(error: OfferTypes.InvoiceError) extends OnionMessagePayloadTlv + } object MessageOnion { @@ -62,6 +83,9 @@ object MessageOnion { case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload { val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath] val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data + val invoiceRequest: Option[OnionMessagePayloadTlv.InvoiceRequest] = records.get[OnionMessagePayloadTlv.InvoiceRequest] + val invoice: Option[OnionMessagePayloadTlv.Invoice] = records.get[OnionMessagePayloadTlv.Invoice] + val invoiceError: Option[OnionMessagePayloadTlv.InvoiceError] = records.get[OnionMessagePayloadTlv.InvoiceError] } /** Content of the encrypted data of a final node's per-hop payload. */ @@ -90,19 +114,25 @@ object MessageOnionCodecs { private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint) .typecase(UInt64(2), replyPathCodec) .typecase(UInt64(4), encryptedDataCodec) + .typecase(UInt64(64), variableSizeBytesLong(varintoverflow, invoiceRequestCodec.as[InvoiceRequest])) + .typecase(UInt64(66), variableSizeBytesLong(varintoverflow, invoiceCodec.as[Invoice])) + .typecase(UInt64(68), variableSizeBytesLong(varintoverflow, invoiceErrorCodec.as[InvoiceError])) + + + val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.tlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete - val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete + val prefixedPerHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = variableSizeBytesLong(CommonCodecs.varintoverflow, perHopPayloadCodec) - val relayPerHopPayloadCodec: Codec[RelayPayload] = perHopPayloadCodec.narrow({ - case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + val relayPerHopPayloadCodec: Codec[RelayPayload] = prefixedPerHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs if tlvs.get[ReplyPath].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(2))) case tlvs => Attempt.successful(RelayPayload(tlvs)) }, { case RelayPayload(tlvs) => tlvs }) - val finalPerHopPayloadCodec: Codec[FinalPayload] = perHopPayloadCodec.narrow({ - case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + val finalPerHopPayloadCodec: Codec[FinalPayload] = prefixedPerHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs => Attempt.successful(FinalPayload(tlvs)) }, { case FinalPayload(tlvs) => tlvs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala new file mode 100644 index 0000000000..6652c917ae --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -0,0 +1,222 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv +import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64} +import scodec.codecs._ +import scodec.{Attempt, Codec} + +object OfferCodecs { + private val chains: Codec[Chains] = variableSizeBytesLong(varintoverflow, list(bytes32)).xmap[Seq[ByteVector32]](_.toSeq, _.toList).as[Chains] + + private val currency: Codec[Currency] = variableSizeBytesLong(varintoverflow, utf8).as[Currency] + + private val amount: Codec[Amount] = variableSizeBytesLong(varintoverflow, tmillisatoshi).as[Amount] + + private val description: Codec[Description] = variableSizeBytesLong(varintoverflow, utf8).as[Description] + + private val features: Codec[FeaturesTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap[Features[Feature]](Features(_), _.toByteVector).as[FeaturesTlv] + + private val absoluteExpiry: Codec[AbsoluteExpiry] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[AbsoluteExpiry] + + private val blindedNodeCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode] + + private val pathCodec: Codec[BlindedRoute] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | listOfN(uint8, blindedNodeCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute] + + private val paths: Codec[Paths] = variableSizeBytesLong(varintoverflow, list(pathCodec)).xmap[Seq[BlindedRoute]](_.toSeq, _.toList).as[Paths] + + private val issuer: Codec[Issuer] = variableSizeBytesLong(varintoverflow, utf8).as[Issuer] + + private val quantityMin: Codec[QuantityMin] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMin] + + private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax] + + private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, publicKey).as[NodeId] + + private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice())) + + private val refundFor: Codec[RefundFor] = variableSizeBytesLong(varintoverflow, bytes32).as[RefundFor] + + private val signature: Codec[Signature] = variableSizeBytesLong(varintoverflow, bytes64).as[Signature] + + val offerTlvCodec: Codec[TlvStream[OfferTlv]] = TlvCodecs.tlvStream[OfferTlv](discriminated[OfferTlv].by(varint) + .typecase(UInt64(2), chains) + .typecase(UInt64(6), currency) + .typecase(UInt64(8), amount) + .typecase(UInt64(10), description) + .typecase(UInt64(12), features) + .typecase(UInt64(14), absoluteExpiry) + .typecase(UInt64(16), paths) + .typecase(UInt64(20), issuer) + .typecase(UInt64(22), quantityMin) + .typecase(UInt64(24), quantityMax) + .typecase(UInt64(30), nodeId) + .typecase(UInt64(34), refundFor) + .typecase(UInt64(54), sendInvoice) + .typecase(UInt64(240), signature)).complete + + val offerCodec: Codec[Offer] = offerTlvCodec.narrow({ tlvs => + if (tlvs.get[Description].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(10))) + } + if (tlvs.get[NodeId].isEmpty && tlvs.get[Paths].forall(_.paths.isEmpty)) { + Attempt.failure(MissingRequiredTlv(UInt64(30))) + } + Attempt.successful(Offer(tlvs)) + }, { + case Offer(tlvs) => tlvs + }) + + private val chain: Codec[Chain] = variableSizeBytesLong(varintoverflow, bytes32).as[Chain] + + private val offerId: Codec[OfferId] = variableSizeBytesLong(varintoverflow, bytes32).as[OfferId] + + private val quantity: Codec[Quantity] = variableSizeBytesLong(varintoverflow, tu64overflow).as[Quantity] + + private val payerKey: Codec[PayerKey] = variableSizeBytesLong(varintoverflow, bytes32).as[PayerKey] + + private val payerNote: Codec[PayerNote] = variableSizeBytesLong(varintoverflow, utf8).as[PayerNote] + + private val payerInfo: Codec[PayerInfo] = variableSizeBytesLong(varintoverflow, bytes).as[PayerInfo] + + private val replaceInvoice: Codec[ReplaceInvoice] = variableSizeBytesLong(varintoverflow, bytes32).as[ReplaceInvoice] + + val invoiceRequestTlvCodec: Codec[TlvStream[InvoiceRequestTlv]] = TlvCodecs.tlvStream[InvoiceRequestTlv](discriminated[InvoiceRequestTlv].by(varint) + .typecase(UInt64(3), chain) + .typecase(UInt64(4), offerId) + .typecase(UInt64(8), amount) + .typecase(UInt64(12), features) + .typecase(UInt64(32), quantity) + .typecase(UInt64(38), payerKey) + .typecase(UInt64(39), payerNote) + .typecase(UInt64(50), payerInfo) + .typecase(UInt64(56), replaceInvoice) + .typecase(UInt64(240), signature)).complete + + val invoiceRequestCodec: Codec[InvoiceRequest] = invoiceRequestTlvCodec.narrow({ tlvs => + if (tlvs.get[OfferId].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(4))) + } else if (tlvs.get[PayerKey].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(38))) + } else if (tlvs.get[Signature].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(240))) + } else { + Attempt.successful(InvoiceRequest(tlvs)) + } + }, { + case InvoiceRequest(tlvs) => tlvs + }) + + private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: + ("fee_proportional_millionths" | uint32) :: + ("cltv_expiry_delta" | cltvExpiryDelta) :: + ("htlc_minimum_msat" | millisatoshi) :: + ("htlc_maximum_msat" | millisatoshi) :: + ("features" | variableSizeBytesLong(uint32, bytes).xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentInfo] + + private val paymentPathsInfo: Codec[PaymentPathsInfo] = variableSizeBytesLong(varintoverflow, list(paymentInfo)).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo] + + private val paymentPathsCapacities: Codec[PaymentPathsCapacities] = variableSizeBytesLong(varintoverflow, list(millisatoshi)).xmap[Seq[MilliSatoshi]](_.toSeq, _.toList).as[PaymentPathsCapacities] + + private val createdAt: Codec[CreatedAt] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[CreatedAt] + + private val paymentHash: Codec[PaymentHash] = variableSizeBytesLong(varintoverflow, bytes32).as[PaymentHash] + + private val relativeExpiry: Codec[RelativeExpiry] = variableSizeBytesLong(varintoverflow, tu32).as[RelativeExpiry] + + private val cltv: Codec[Cltv] = variableSizeBytesLong(varintoverflow, uint16).as[CltvExpiryDelta].as[Cltv] + + private val fallbackAddress: Codec[FallbackAddress] = variableSizeBytesLong(varintoverflow, + ("version" | byte) :: + ("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress] + + private val fallbacks: Codec[Fallbacks] = variableSizeBytesLong(varintoverflow, list(fallbackAddress)).xmap[Seq[FallbackAddress]](_.toSeq, _.toList).as[Fallbacks] + + private val refundSignature: Codec[RefundSignature] = variableSizeBytesLong(varintoverflow, bytes64).as[RefundSignature] + + val invoiceTlvCodec: Codec[TlvStream[InvoiceTlv]] = TlvCodecs.tlvStream[InvoiceTlv](discriminated[InvoiceTlv].by(varint) + .typecase(UInt64(3), chain) + .typecase(UInt64(4), offerId) + .typecase(UInt64(8), amount) + .typecase(UInt64(10), description) + .typecase(UInt64(12), features) + // TODO: the spec for payment paths is not final, adjust codecs if changes are made to he spec. + .typecase(UInt64(16), paths) + .typecase(UInt64(18), paymentPathsInfo) + .typecase(UInt64(19), paymentPathsCapacities) + .typecase(UInt64(20), issuer) + .typecase(UInt64(30), nodeId) + .typecase(UInt64(32), quantity) + .typecase(UInt64(34), refundFor) + .typecase(UInt64(38), payerKey) + .typecase(UInt64(39), payerNote) + .typecase(UInt64(40), createdAt) + .typecase(UInt64(42), paymentHash) + .typecase(UInt64(44), relativeExpiry) + .typecase(UInt64(46), cltv) + .typecase(UInt64(48), fallbacks) + .typecase(UInt64(50), payerInfo) + .typecase(UInt64(52), refundSignature) + .typecase(UInt64(56), replaceInvoice) + .typecase(UInt64(240), signature) + ).complete + + val invoiceCodec: Codec[Bolt12Invoice] = invoiceTlvCodec.narrow({ tlvs => + if (tlvs.get[Amount].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(8))) + } else if (tlvs.get[Description].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(10))) + } else if (tlvs.get[Paths].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(16))) + } else if (tlvs.get[NodeId].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(30))) + } else if (tlvs.get[CreatedAt].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(40))) + } else if (tlvs.get[PaymentHash].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(42))) + } else if (tlvs.get[Signature].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(240))) + } else { + Attempt.successful(Bolt12Invoice(tlvs)) + } + }, { + case Bolt12Invoice(tlvs) => tlvs + }) + + val invoiceErrorTlvCodec: Codec[TlvStream[InvoiceErrorTlv]] = TlvCodecs.tlvStream[InvoiceErrorTlv](discriminated[InvoiceErrorTlv].by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, tu64overflow).as[ErroneousField]) + .typecase(UInt64(3), variableSizeBytesLong(varintoverflow, bytes).as[SuggestedValue]) + .typecase(UInt64(5), variableSizeBytesLong(varintoverflow, utf8).as[Error]) + ).complete + + val invoiceErrorCodec: Codec[InvoiceError] = invoiceErrorTlvCodec.narrow({ tlvs => + if (tlvs.get[Error].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(5))) + } else { + Attempt.successful(InvoiceError(tlvs)) + } + }, { + case InvoiceError(tlvs) => tlvs + }) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala new file mode 100644 index 0000000000..a9ab3671f4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -0,0 +1,383 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.Bech32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute +import fr.acinq.eclair.wire.protocol.OfferCodecs._ +import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} +import fr.acinq.secp256k1.Secp256k1JvmKt +import scodec.Codec +import scodec.bits.ByteVector +import scodec.codecs.vector + +import scala.util.Try + +/** + * Lightning Bolt 12 offers + * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + */ +object OfferTypes { + + sealed trait Bolt12Tlv extends Tlv + + sealed trait OfferTlv extends Bolt12Tlv + + sealed trait InvoiceRequestTlv extends Bolt12Tlv + + sealed trait InvoiceTlv extends Bolt12Tlv + + sealed trait InvoiceErrorTlv extends Bolt12Tlv + + case class Chains(chains: Seq[ByteVector32]) extends OfferTlv + + case class Currency(iso4217: String) extends OfferTlv + + case class Amount(amount: MilliSatoshi) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class Description(description: String) extends OfferTlv with InvoiceTlv + + case class FeaturesTlv(features: Features[Feature]) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class AbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv + + case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv + + case class PaymentInfo(feeBase: MilliSatoshi, + feeProportionalMillionths: Long, + cltvExpiryDelta: CltvExpiryDelta, + minHtlc: MilliSatoshi, + maxHtlc: MilliSatoshi, + allowedFeatures: Features[Feature]) + case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv + + case class PaymentPathsCapacities(capacities: Seq[MilliSatoshi]) extends InvoiceTlv + + case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv + + case class QuantityMin(min: Long) extends OfferTlv + + case class QuantityMax(max: Long) extends OfferTlv + + case class NodeId(publicKey: PublicKey) extends OfferTlv with InvoiceTlv + + case class SendInvoice() extends OfferTlv with InvoiceTlv + + case class RefundFor(refundedPaymentHash: ByteVector32) extends OfferTlv with InvoiceTlv + + case class Signature(signature: ByteVector64) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class Chain(hash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class OfferId(offerId: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class Quantity(quantity: Long) extends InvoiceRequestTlv with InvoiceTlv + + case class PayerKey(publicKey: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + object PayerKey { + def apply(publicKey: PublicKey): PayerKey = PayerKey(xOnlyPublicKey(publicKey)) + } + + case class PayerNote(note: String) extends InvoiceRequestTlv with InvoiceTlv + + case class PayerInfo(info: ByteVector) extends InvoiceRequestTlv with InvoiceTlv + + case class ReplaceInvoice(paymentHash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class CreatedAt(timestamp: TimestampSecond) extends InvoiceTlv + + case class PaymentHash(hash: ByteVector32) extends InvoiceTlv + + case class RelativeExpiry(seconds: Long) extends InvoiceTlv + + case class Cltv(minFinalCltvExpiry: CltvExpiryDelta) extends InvoiceTlv + + case class FallbackAddress(version: Byte, value: ByteVector) + + case class Fallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv + + case class RefundSignature(signature: ByteVector64) extends InvoiceTlv + + case class ErroneousField(tag: Long) extends InvoiceErrorTlv + + case class SuggestedValue(value: ByteVector) extends InvoiceErrorTlv + + case class Error(message: String) extends InvoiceErrorTlv + + case class Offer(records: TlvStream[OfferTlv]) { + + require(records.get[NodeId].nonEmpty || records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 offers must provide a node id or a blinded path") + require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description") + + val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec) + + val chains: Seq[ByteVector32] = records.get[Chains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash)) + + val currency: Option[String] = records.get[Currency].map(_.iso4217) + + val amount: Option[MilliSatoshi] = currency match { + case Some(_) => None // TODO: add exchange rates + case None => records.get[Amount].map(_.amount) + } + + val description: String = records.get[Description].get.description + + val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + val expiry: Option[TimestampSecond] = records.get[AbsoluteExpiry].map(_.absoluteExpiry) + + val issuer: Option[String] = records.get[Issuer].map(_.issuer) + + val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min) + val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max) + + val nodeId: PublicKey = + records.get[NodeId].map(_.publicKey).getOrElse(records.get[Paths].get.paths.head.blindedNodes.last.blindedPublicKey) + + val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty + + val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash) + + val signature: Option[ByteVector64] = records.get[Signature].map(_.signature) + + val contact: Either[Seq[BlindedRoute], PublicKey] = + records.get[Paths].map(_.paths).map(Left(_)).getOrElse(Right(records.get[NodeId].get.publicKey)) + + def sign(key: PrivateKey): Offer = { + val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key) + Offer(TlvStream[OfferTlv](records.records ++ Seq(Signature(sig)), records.unknown)) + } + + def checkSignature(): Boolean = { + signature match { + case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, xOnlyPublicKey(nodeId)) + case None => false + } + } + + def encode(): String = { + val data = offerCodec.encode(this).require.bytes + Bech32.encodeBytes(Offer.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + override def toString: String = encode() + } + + object Offer { + val hrp = "lno" + val signatureTag: String = "lightning" + "offer" + "signature" + + /** + * @param amount_opt amount if it can be determined at offer creation time. + * @param description description of the offer. + * @param nodeId the nodeId to use for this offer, which should be different from our public nodeId if we're hiding behind a blinded route. + * @param features invoice features. + * @param chain chain on which the offer is valid. + */ + def apply(amount_opt: Option[MilliSatoshi], description: String, nodeId: PublicKey, features: Features[InvoiceFeature], chain: ByteVector32): Offer = { + val tlvs: Seq[OfferTlv] = Seq( + if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None, + amount_opt.map(Amount), + Some(Description(description)), + Some(NodeId(nodeId)), + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, + ).flatten + Offer(TlvStream(tlvs)) + } + + def decode(s: String): Try[Offer] = Try { + val triple = Bech32.decodeBytes(s.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + offerCodec.decode(ByteVector(encoded).bits).require.value + } + } + + case class InvoiceRequest(records: TlvStream[InvoiceRequestTlv]) { + + require(records.get[OfferId].nonEmpty, "bolt 12 invoice requests must provide an offer id") + require(records.get[PayerKey].nonEmpty, "bolt 12 invoice requests must provide a payer key") + require(records.get[Signature].nonEmpty, "bolt 12 invoice requests must provide a payer signature") + + val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) + + val offerId: ByteVector32 = records.get[OfferId].map(_.offerId).get + + val amount: Option[MilliSatoshi] = records.get[Amount].map(_.amount) + + val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + val quantity_opt: Option[Long] = records.get[Quantity].map(_.quantity) + + val quantity: Long = quantity_opt.getOrElse(1) + + val payerKey: ByteVector32 = records.get[PayerKey].get.publicKey + + val payerNote: Option[String] = records.get[PayerNote].map(_.note) + + val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info) + + val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash) + + val payerSignature: ByteVector64 = records.get[Signature].get.signature + + def isValidFor(offer: Offer): Boolean = { + val amountOk = offer.amount match { + case Some(offerAmount) => + val baseInvoiceAmount = offerAmount * quantity + amount.forall(baseInvoiceAmount <= _) + case None => amount.nonEmpty + } + amountOk && + offer.offerId == offerId && + offer.chains.contains(chain) && + offer.quantityMin.forall(min => quantity_opt.nonEmpty && min <= quantity) && + offer.quantityMax.forall(max => quantity_opt.nonEmpty && quantity <= max) && + quantity_opt.forall(_ => offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) && + offer.features.areSupported(features) && + checkSignature() + } + + def checkSignature(): Boolean = { + verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), invoiceRequestTlvCodec), payerSignature, payerKey) + } + + def encode(): String = { + val data = invoiceRequestCodec.encode(this).require.bytes + Bech32.encodeBytes(InvoiceRequest.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + override def toString: String = encode() + } + + object InvoiceRequest { + val hrp = "lnr" + val signatureTag: String = "lightning" + "invoice_request" + "payer_signature" + + /** + * Create a request to fetch an invoice for a given offer. + * + * @param offer Bolt 12 offer. + * @param amount amount that we want to pay. + * @param quantity quantity of items we're buying. + * @param features invoice features. + * @param payerKey private key identifying the payer: this lets us prove we're the ones who paid the invoice. + * @param chain chain we want to use to pay this offer. + */ + def apply(offer: Offer, amount: MilliSatoshi, quantity: Long, features: Features[InvoiceFeature], payerKey: PrivateKey, chain: ByteVector32): InvoiceRequest = { + require(offer.chains contains chain) + require(quantity == 1 || offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) + val tlvs: Seq[InvoiceRequestTlv] = Seq( + Some(Chain(chain)), + Some(OfferId(offer.offerId)), + Some(Amount(amount)), + if (offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) Some(Quantity(quantity)) else None, + Some(PayerKey(payerKey.publicKey)), + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, + ).flatten + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + + def decode(s: String): Try[InvoiceRequest] = Try { + val triple = Bech32.decodeBytes(s.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + invoiceRequestCodec.decode(ByteVector(encoded).bits).require.value + } + } + + case class InvoiceError(records: TlvStream[InvoiceErrorTlv]) { + require(records.get[Error].nonEmpty, "bolt 12 invoice errors must provide an explanatory string") + } + + def rootHash[T <: Tlv](tlvs: TlvStream[T], codec: Codec[TlvStream[T]]): ByteVector32 = { + // Encoding tlvs is always safe, unless we have a bug in our codecs, so we can call `.require` here. + val encoded = codec.encode(tlvs).require + // Decoding tlvs that we just encoded is safe as well. + // This encoding/decoding step ensures that the resulting tlvs are ordered. + val genericTlvs = vector(genericTlv).decode(encoded).require.value + val nonceKey = ByteVector("LnAll".getBytes) ++ encoded.bytes + + def previousPowerOfTwo(n: Int): Int = { + var p = 1 + while (p < n) { + p = p << 1 + } + p >> 1 + } + + def merkleTree(i: Int, j: Int): ByteVector32 = { + val (a, b) = if (j - i == 1) { + val tlv = genericTlv.encode(genericTlvs(i)).require.bytes + (hash(ByteVector("LnLeaf".getBytes), tlv), hash(nonceKey, tlv)) + } else { + val k = i + previousPowerOfTwo(j - i) + (merkleTree(i, k), merkleTree(k, j)) + } + if (LexicographicalOrdering.isLessThan(a, b)) { + hash(ByteVector("LnBranch".getBytes), a ++ b) + } else { + hash(ByteVector("LnBranch".getBytes), b ++ a) + } + } + + merkleTree(0, genericTlvs.length) + } + + private def hash(tag: String, msg: ByteVector): ByteVector32 = { + ByteVector.encodeAscii(tag) match { + case Right(bytes) => hash(bytes, msg) + case Left(e) => throw e // NB: the tags we use are hard-coded, so we know they're always ASCII + } + } + + private def hash(tag: ByteVector, msg: ByteVector): ByteVector32 = { + val tagHash = Crypto.sha256(tag) + Crypto.sha256(tagHash ++ tagHash ++ msg) + } + + def signSchnorr(tag: String, msg: ByteVector32, key: PrivateKey): ByteVector64 = { + val h = hash(tag, msg) + // NB: we don't add auxiliary random data to keep signatures deterministic. + ByteVector64(ByteVector(Secp256k1JvmKt.getSecpk256k1.signSchnorr(h.toArray, key.value.toArray, null))) + } + + def verifySchnorr(tag: String, msg: ByteVector32, signature: ByteVector64, publicKey: ByteVector32): Boolean = { + val h = hash(tag, msg) + Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, publicKey.toArray) + } + + def xOnlyPublicKey(publicKey: PublicKey): ByteVector32 = ByteVector32(publicKey.value.drop(1)) + + /** We often need to remove the signature field to compute the merkle root. */ + def removeSignature[T <: Bolt12Tlv](records: TlvStream[T]): TlvStream[T] = { + TlvStream(records.records.filter { case _: Signature => false case _ => true }, records.unknown) + } + +} + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala index ed40d2f0ca..9cfdf679f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol.CommonCodecs.bytes32 import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 93fa55e7a9..ab18778971 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.TlvCodecs._ @@ -171,7 +171,7 @@ object OnionPaymentPayloadTlv { * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. */ - case class InvoiceRoutingInfo(extraHops: List[List[PaymentRequest.ExtraHop]]) extends OnionPaymentPayloadTlv + case class InvoiceRoutingInfo(extraHops: List[List[Bolt11Invoice.ExtraHop]]) extends OnionPaymentPayloadTlv /** An encrypted trampoline onion packet. */ case class TrampolineOnion(packet: OnionRoutingPacket) extends OnionPaymentPayloadTlv @@ -199,20 +199,9 @@ object PaymentOnion { * / \ \ \ * RelayLegacyPayload ChannelRelayTlvPayload NodeRelayPayload FinalTlvPayload * - * We also introduce additional traits to separate payloads based on their encoding (PerHopPayloadFormat) and on the - * type of onion packet they can be used with (PacketType). + * We also introduce additional traits to separate payloads based on the type of onion packet they can be used with (PacketType). */ - sealed trait PerHopPayloadFormat - - /** Legacy fixed-size 65-bytes onion payload. */ - sealed trait LegacyFormat extends PerHopPayloadFormat - - /** Variable-length onion payload with optional additional tlv records. */ - sealed trait TlvFormat extends PerHopPayloadFormat { - def records: TlvStream[OnionPaymentPayloadTlv] - } - /** Payment onion packet type. */ sealed trait PacketType @@ -229,7 +218,7 @@ object PaymentOnion { sealed trait PerHopPayload /** Per-hop payload for an intermediate node. */ - sealed trait RelayPayload extends PerHopPayload with PerHopPayloadFormat { + sealed trait RelayPayload extends PerHopPayload { /** Amount to forward to the next node. */ val amountToForward: MilliSatoshi /** CLTV value to use for the HTLC offered to the next node. */ @@ -242,18 +231,18 @@ object PaymentOnion { } /** Per-hop payload for a final node. */ - sealed trait FinalPayload extends PerHopPayload with PerHopPayloadFormat with TrampolinePacket with PaymentPacket { + sealed trait FinalPayload extends PerHopPayload with TrampolinePacket with PaymentPacket { val amount: MilliSatoshi val expiry: CltvExpiry - val paymentSecret: ByteVector32 + val paymentSecret: Option[ByteVector32] val totalAmount: MilliSatoshi val paymentPreimage: Option[ByteVector32] val paymentMetadata: Option[ByteVector] } - case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat + case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload - case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload with TlvFormat { + case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload { override val amountToForward = records.get[AmountToForward].get.amount override val outgoingCltv = records.get[OutgoingCltv].get.cltv override val outgoingChannelId = records.get[OutgoingChannelId].get.shortChannelId @@ -264,7 +253,7 @@ object PaymentOnion { ChannelRelayTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId))) } - case class NodeRelayPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends RelayPayload with TlvFormat with TrampolinePacket { + case class NodeRelayPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends RelayPayload with TrampolinePacket { val amountToForward = records.get[AmountToForward].get.amount val outgoingCltv = records.get[OutgoingCltv].get.cltv val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId @@ -279,10 +268,10 @@ object PaymentOnion { val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops) } - case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload with TlvFormat { + case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload { override val amount = records.get[AmountToForward].get.amount override val expiry = records.get[OutgoingCltv].get.cltv - override val paymentSecret = records.get[PaymentData].get.secret + override val paymentSecret = records.get[PaymentData].map(_.secret) override val totalAmount = records.get[PaymentData].map(_.totalAmount match { case MilliSatoshi(0) => amount case totalAmount => totalAmount @@ -295,7 +284,8 @@ object PaymentOnion { NodeRelayPayload(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId))) /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ - def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = { + // TODO: Allow sending blinded routes to trampoline nodes instead of routing hints to support BOLT12Invoice + def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): NodeRelayPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), @@ -308,21 +298,21 @@ object PaymentOnion { NodeRelayPayload(TlvStream(tlvs)) } - def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { + def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: Option[ByteVector32], paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), - Some(PaymentData(paymentSecret, amount)), + paymentSecret.map(PaymentData(_, amount)), paymentMetadata.map(m => PaymentMetadata(m)) ).flatten FinalTlvPayload(TlvStream(tlvs, userCustomTlvs)) } - def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { + def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: Option[ByteVector32], paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), - Some(PaymentData(paymentSecret, totalAmount)), + paymentSecret.map(PaymentData(_, totalAmount)), paymentMetadata.map(m => PaymentMetadata(m)) ).flatten FinalTlvPayload(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs)) @@ -374,7 +364,7 @@ object PaymentOnionCodecs { private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures] - private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo] + private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, Bolt11Invoice.Codecs.extraHopCodec))).as[InvoiceRoutingInfo] private val trampolineOnion: Codec[TrampolineOnion] = variableSizeBytesLong(varintoverflow, trampolineOnionPacketCodec).as[TrampolineOnion] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala index 546383ea72..8244416afc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala index 881077fdbf..c1c04307b4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index 1797eb225d..67d7836029 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{minimalvalue, uint64, varint, varintoverflow} import fr.acinq.eclair.{MilliSatoshi, UInt64} @@ -77,6 +78,9 @@ object TlvCodecs { */ val tmillisatoshi: Codec[MilliSatoshi] = tu64overflow.xmap(l => MilliSatoshi(l), m => m.toLong) + /** Truncated satoshi (0 to 8 bytes unsigned). */ + val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong) + /** Truncated uint32 (0 to 4 bytes unsigned integer). */ val tu32: Codec[Long] = tu64.exmap({ case i if i > 0xffffffffL => Attempt.Failure(Err("tu32 overflow")) @@ -104,7 +108,7 @@ object TlvCodecs { /** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */ val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16) - private def validateGenericTlv(g: GenericTlv): Attempt[GenericTlv] = { + private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = { if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) { Attempt.Failure(Err("unknown even tlv type")) } else { @@ -112,7 +116,9 @@ object TlvCodecs { } } - val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv].exmap(validateGenericTlv, validateGenericTlv) + val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv] + + private val unknownTlv = genericTlv.exmap[GenericTlv](validateUnknownTlv, validateUnknownTlv) private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match { case Left(generic) => generic.tag @@ -140,7 +146,7 @@ object TlvCodecs { * @param codec codec used for the tlv records contained in the stream. * @tparam T stream namespace. */ - def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(genericTlv, codec)).exmap( + def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(unknownTlv, codec)).exmap( records => validateStream(codec, records), (stream: TlvStream[T]) => { val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList diff --git a/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java index e48c72cf9e..388c8d5774 100644 --- a/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java +++ b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java @@ -16,7 +16,7 @@ package fr.acinq.eclair; -import fr.acinq.bitcoin.Satoshi; +import fr.acinq.bitcoin.scalacompat.Satoshi; /** * This class is a compile-time check that we are able to compile Java code that uses MilliSatoshi utilities. diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index 21441ee3ad..91441698aa 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -20,7 +20,7 @@ System.out - %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%.-9X{nodeId}%.-11X{channelId}%.-11X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId}%.-11X{nodeAlias} - %msg%ex{12}%n diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin new file mode 100644 index 0000000000..e47310bccf --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin @@ -0,0 +1 @@ +00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json new file mode 100644 index 0000000000..d92f759936 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json @@ -0,0 +1,150 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c", + "channelConfig" : [ ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 1457788542, 1007597768, 1455922339, 479707306 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 5000000000, + "requestedChannelReserve_opt" : 167772, + "htlcMinimum" : 1, + "toSelfDelay" : 720, + "maxAcceptedHtlcs" : 30, + "isInitiator" : false, + "defaultFinalScriptPubKey" : "a9144805d016e47885dc7c852710cdd8cd0d576f57ec87", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "optional", + "initial_routing_sync" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 16609443000, + "requestedChannelReserve_opt" : 167772, + "htlcMinimum" : 1000, + "toSelfDelay" : 2016, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64", + "revocationBasepoint" : "02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1", + "paymentBasepoint" : "034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1", + "delayedPaymentBasepoint" : "0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467", + "htlcBasepoint" : "03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "mandatory", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 7675, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 254, + "toLocal" : 204739729, + "toRemote" : 16572475271 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4", + "tx" : "020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620" + }, + "remoteSig" : "4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 7779, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 254, + "toLocal" : 16572475271, + "toRemote" : 204739729 + }, + "txid" : "ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be", + "remotePerCommitmentPoint" : "03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 203, + "remoteNextHtlcId" : 4147, + "originChannels" : { }, + "remoteNextCommitInfo" : "034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6", + "commitInput" : { + "outPoint" : "3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1", + "amountSatoshis" : 16777215 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "1513532x23x1" + }, + "localAlias" : "0x17183c0000170001" + }, + "channelAnnouncement" : { + "nodeSignature1" : "d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174", + "nodeSignature2" : "075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321", + "bitcoinSignature1" : "4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5", + "bitcoinSignature2" : "0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70", + "features" : { + "activated" : { }, + "unknown" : [ ] + }, + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1513532x23x1", + "nodeId1" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", + "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "bitcoinKey1" : "028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64", + "bitcoinKey2" : "03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + }, + "channelUpdate" : { + "signature" : "4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1513532x23x1", + "timestamp" : { + "iso" : "2019-06-18T12:49:33Z", + "unix" : 1560862173 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 16777215000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin new file mode 100644 index 0000000000..cace3d5deb --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin @@ -0,0 +1 @@ +00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json new file mode 100644 index 0000000000..79f8c7a9c8 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json @@ -0,0 +1,130 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611", + "channelConfig" : [ ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 3561221353, 3653515793, 2711311691, 2863050005 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 1000000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 30, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "a91445e990148599176534ec9b75df92ace9263f7d3487", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "optional", + "initial_routing_sync" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 14850000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1000, + "toSelfDelay" : 1802, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4", + "revocationBasepoint" : "03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c", + "paymentBasepoint" : "03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd", + "delayedPaymentBasepoint" : "03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397", + "htlcBasepoint" : "034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "mandatory", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 20024, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 750, + "toLocal" : 1343316620, + "toRemote" : 13656683380 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43", + "tx" : "02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320" + }, + "remoteSig" : "bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 20024, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 750, + "toLocal" : 13656683380, + "toRemote" : 1343316620 + }, + "txid" : "919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49", + "remotePerCommitmentPoint" : "02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 9288, + "remoteNextHtlcId" : 151, + "originChannels" : { }, + "remoteNextCommitInfo" : "02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16", + "commitInput" : { + "outPoint" : "115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0", + "amountSatoshis" : 15000000 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "1413373x969x0" + }, + "localAlias" : "0x1590fd0003c90000" + }, + "channelUpdate" : { + "signature" : "52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1413373x969x0", + "timestamp" : { + "iso" : "2019-06-24T09:39:33Z", + "unix" : 1561369173 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 15000000000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin new file mode 100644 index 0000000000..fbdcec643a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin @@ -0,0 +1 @@ +0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json new file mode 100644 index 0000000000..90f79e9c7a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json @@ -0,0 +1,164 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_static_remotekey" ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 2353764507, 3184449568, 2809819526, 3258060413, 392846475, 1545000620, 720603293, 1808318336, 2147483649 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 20000000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 720, + "maxAcceptedHtlcs" : 30, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "00148061b7fbd2d84ed1884177ea785faecb2080b103", + "walletStaticPaymentBasepoint" : "02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3", + "initFeatures" : { + "activated" : { + "option_support_large_channel" : "optional", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "payment_secret" : "optional", + "option_shutdown_anysegwit" : "optional", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 14850000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 1802, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b", + "revocationBasepoint" : "0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75", + "paymentBasepoint" : "03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd", + "delayedPaymentBasepoint" : "03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8", + "htlcBasepoint" : "022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57", + "initFeatures" : { + "activated" : { + "option_upfront_shutdown_script" : "optional", + "payment_secret" : "mandatory", + "option_data_loss_protect" : "mandatory", + "var_onion_optin" : "optional", + "option_static_remotekey" : "mandatory", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 31 ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 4, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 4166, + "toLocal" : 15000000000, + "toRemote" : 0 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f", + "tx" : "02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20" + }, + "remoteSig" : "871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 4, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 4166, + "toLocal" : 0, + "toRemote" : 15000000000 + }, + "txid" : "b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8", + "remotePerCommitmentPoint" : "02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0, + "originChannels" : { }, + "remoteNextCommitInfo" : "03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d", + "commitInput" : { + "outPoint" : "1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0", + "amountSatoshis" : 15000000 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "2026958x1x0" + }, + "localAlias" : "0x1eedce0000010000" + }, + "channelAnnouncement" : { + "nodeSignature1" : "98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969", + "nodeSignature2" : "92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0", + "bitcoinSignature1" : "9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f", + "bitcoinSignature2" : "84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b", + "features" : { + "activated" : { }, + "unknown" : [ ] + }, + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "2026958x1x0", + "nodeId1" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", + "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "bitcoinKey1" : "02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b", + "bitcoinKey2" : "023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + }, + "channelUpdate" : { + "signature" : "710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "2026958x1x0", + "timestamp" : { + "iso" : "2021-07-08T12:09:56Z", + "unix" : 1625746196 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 15000000000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin new file mode 100644 index 0000000000..0f0ba269a5 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin @@ -0,0 +1 @@ +030000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f401010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f3ef2134fbf50054ec0414b18905a8d87d8b0931410173e35923fa5d4c5a3f5280000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014fec406ef7a0258cb503fe1f1803787d971eeb4d10000186b020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002498202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb28302f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe1002be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab83403fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a08740000000302498200000000000000000000000000002710000000002faf0800000000000bebc20024e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae7d0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b72621201ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b400000000000000000000000000002710000000000bebc200000000002faf080010e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee000000000000000000000000000000000000000000000000000000000000ff030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f24e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae000000ff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b3000000000000000000061a800000820000000000000000000000000000000000000000000000000000000000000000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json new file mode 100644 index 0000000000..548e30349a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json @@ -0,0 +1,123 @@ +{ + "type" : "DATA_WAIT_FOR_FUNDING_CONFIRMED", + "commitments" : { + "channelId" : "e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : { + "path" : [ 4092535092, 4227137620, 3959690417, 2298849496, 2106263857, 1090614243, 1495530077, 1280982866, 2147483649 ] + }, + "dustLimit" : 1100, + "maxHtlcValueInFlightMsat" : 500000000, + "requestedChannelReserve_opt" : 10000, + "htlcMinimum" : 0, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 100, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "0014fec406ef7a0258cb503fe1f1803787d971eeb4d1", + "initFeatures" : { + "activated" : { + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "dustLimit" : 1000, + "maxHtlcValueInFlightMsat" : 18446744073709551615, + "requestedChannelReserve_opt" : 20000, + "htlcMinimum" : 1000, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 30, + "fundingPubKey" : "02e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb283", + "revocationBasepoint" : "02f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe10", + "paymentBasepoint" : "02be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab834", + "delayedPaymentBasepoint" : "03fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7", + "htlcBasepoint" : "021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a0874", + "initFeatures" : { + "activated" : { + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : false + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 10000, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "c6fe6bc0a5a9c149a03a907d2351714aa27fc98a485e981343cea08a1904ee26", + "tx" : "0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b7262120" + }, + "remoteSig" : "1ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b4" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 10000, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txid" : "10e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290", + "remotePerCommitmentPoint" : "032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0, + "originChannels" : { }, + "remoteNextCommitInfo" : "030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f", + "commitInput" : { + "outPoint" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9:0", + "amountSatoshis" : 1000000 + }, + "remotePerCommitmentSecrets" : null + }, + "fundingTx" : { + "txid" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9", + "tx" : "020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b300000000" + }, + "waitingSince" : 400000, + "lastSent" : { + "temporaryChannelId" : "0000000000000000000000000000000000000000000000000000000000000000", + "fundingTxid" : "e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4", + "fundingOutputIndex" : 0, + "signature" : "bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala index e77446a728..b319a494d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala @@ -27,15 +27,15 @@ class CltvExpirySpec extends AnyFunSuite with ParallelTestExecution { test("cltv expiry delta") { val d = CltvExpiryDelta(561) - assert(d.toInt === 561) + assert(d.toInt == 561) // add - assert(d + 5 === CltvExpiryDelta(566)) - assert(d + CltvExpiryDelta(5) === CltvExpiryDelta(566)) + assert(d + 5 == CltvExpiryDelta(566)) + assert(d + CltvExpiryDelta(5) == CltvExpiryDelta(566)) // subtract - assert(d - CltvExpiryDelta(5) === CltvExpiryDelta(556)) - assert(d - CltvExpiryDelta(562) === CltvExpiryDelta(-1)) + assert(d - CltvExpiryDelta(5) == CltvExpiryDelta(556)) + assert(d - CltvExpiryDelta(562) == CltvExpiryDelta(-1)) // compare assert(d <= CltvExpiryDelta(561)) @@ -44,18 +44,18 @@ class CltvExpirySpec extends AnyFunSuite with ParallelTestExecution { assert(d > CltvExpiryDelta(560)) // convert to cltv expiry - assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1105)) === CltvExpiry(1666)) - assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1106)) === CltvExpiry(1667)) + assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1105)) == CltvExpiry(1666)) + assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1106)) == CltvExpiry(1667)) } test("cltv expiry") { val e = CltvExpiry(1105) - assert(e.toLong === 1105) + assert(e.toLong == 1105) // add - assert(e + CltvExpiryDelta(561) === CltvExpiry(1666)) - assert(e - CltvExpiryDelta(561) === CltvExpiry(544)) - assert(e - CltvExpiry(561) === CltvExpiryDelta(544)) + assert(e + CltvExpiryDelta(561) == CltvExpiry(1666)) + assert(e - CltvExpiryDelta(561) == CltvExpiry(544)) + assert(e - CltvExpiry(561) == CltvExpiryDelta(544)) // compare assert(e <= CltvExpiry(1105)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index b34920df1d..9d0ab122a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,32 +16,35 @@ package fr.acinq.eclair -import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.{ActorRef, Status} import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, SatoshiLong} import fr.acinq.eclair.ApiTypes.{ChannelIdentifier, ChannelNotFound} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.OpenChannel -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.PaymentHandler -import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, RelayFees} import fr.acinq.eclair.payment.send.PaymentInitiator._ -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort import fr.acinq.eclair.router.Router.{PredefinedNodeRoute, PublicChannel} import fr.acinq.eclair.router.{Announcements, Router} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAnnouncement} import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, ParallelTestExecution} import scodec.bits._ @@ -55,7 +58,7 @@ import scala.concurrent.duration._ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with IdiomaticMockito with ParallelTestExecution { implicit val timeout: Timeout = Timeout(30 seconds) - case class FixtureParam(register: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit) + case class FixtureParam(register: TestProbe, relayer: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit) override def withFixture(test: OneArgTest): Outcome = { val watcher = TestProbe() @@ -85,7 +88,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I postman.ref.toTyped, new DummyOnChainWallet() ) - withFixture(test.toNoArgTest(FixtureParam(register, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit))) + withFixture(test.toNoArgTest(FixtureParam(register, relayer, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit))) } test("convert fee rate properly") { f => @@ -95,15 +98,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) + assert(open.fundingTxFeeratePerKw_opt.contains(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) - assert(open1.channelType_opt === Some(ChannelTypes.StaticRemoteKey)) + assert(open1.fundingTxFeeratePerKw_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) + assert(open1.channelType_opt.contains(ChannelTypes.StaticRemoteKey)) } test("call send with passing correct arguments") { f => @@ -111,55 +114,60 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) val nodePrivKey = randomKey() - val invoice0 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) + val invoice0 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) eclair.send(None, 123 msat, invoice0) val send = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send.externalId === None) - assert(send.recipientNodeId === nodePrivKey.publicKey) - assert(send.recipientAmount === 123.msat) - assert(send.paymentHash === ByteVector32.Zeroes) - assert(send.paymentRequest === invoice0) - assert(send.assistedRoutes === Seq.empty) + assert(send.externalId.isEmpty) + assert(send.recipientNodeId == nodePrivKey.publicKey) + assert(send.recipientAmount == 123.msat) + assert(send.paymentHash == ByteVector32.Zeroes) + assert(send.invoice == invoice0) + assert(send.extraEdges == Seq.empty) // with assisted routes val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" - val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val invoice1 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) + val hints = List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId.fromCoordinates("569178x2331x1").success.value, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) + val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, List(hints)) eclair.send(Some(externalId1), 123 msat, invoice1) val send1 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send1.externalId === Some(externalId1)) - assert(send1.recipientNodeId === nodePrivKey.publicKey) - assert(send1.recipientAmount === 123.msat) - assert(send1.paymentHash === ByteVector32.Zeroes) - assert(send1.paymentRequest === invoice1) - assert(send1.assistedRoutes === hints) + assert(send1.externalId.contains(externalId1)) + assert(send1.recipientNodeId == nodePrivKey.publicKey) + assert(send1.recipientAmount == 123.msat) + assert(send1.paymentHash == ByteVector32.Zeroes) + assert(send1.invoice == invoice1) + assert(send1.extraEdges.length == 1) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId.fromCoordinates("569178x2331x1").success.value) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == Bob.nodeParams.nodeId) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == nodePrivKey.publicKey) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" - val invoice2 = PaymentRequest("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty) + val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description")), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send2.externalId === Some(externalId2)) - assert(send2.recipientNodeId === nodePrivKey.publicKey) - assert(send2.recipientAmount === 123.msat) - assert(send2.paymentHash === ByteVector32.Zeroes) - assert(send2.paymentRequest === invoice2) - assert(send2.fallbackFinalExpiryDelta === CltvExpiryDelta(96)) + assert(send2.externalId.contains(externalId2)) + assert(send2.recipientNodeId == nodePrivKey.publicKey) + assert(send2.recipientAmount == 123.msat) + assert(send2.paymentHash == ByteVector32.Zeroes) + assert(send2.invoice == invoice2) // with custom route fees parameters eclair.send(None, 123 msat, invoice0, maxFeeFlat_opt = Some(123 sat), maxFeePct_opt = Some(4.20)) val send3 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send3.externalId === None) - assert(send3.recipientNodeId === nodePrivKey.publicKey) - assert(send3.recipientAmount === 123.msat) - assert(send3.paymentHash === ByteVector32.Zeroes) - assert(send3.routeParams.boundaries.maxFeeFlat === 123000.msat) // conversion sat -> msat - assert(send3.routeParams.boundaries.maxFeeProportional === 0.042) + assert(send3.externalId.isEmpty) + assert(send3.recipientNodeId == nodePrivKey.publicKey) + assert(send3.recipientAmount == 123.msat) + assert(send3.paymentHash == ByteVector32.Zeroes) + assert(send3.routeParams.boundaries.maxFeeFlat == 123000.msat) // conversion sat -> msat + assert(send3.routeParams.boundaries.maxFeeProportional == 0.042) val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;" assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), 123 msat, invoice0), 50 millis)) - val expiredInvoice = invoice2.copy(timestamp = 0.unixsec) + val expiredInvoice = invoice2.copy(createdAt = 0.unixsec) assertThrows[IllegalArgumentException](Await.result(eclair.send(None, 123 msat, expiredInvoice), 50 millis)) } @@ -180,13 +188,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.nodes().pipeTo(sender.ref) router.expectMsg(Router.GetNodes) router.reply(allNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet === allNodes.toSet) + assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet == allNodes.toSet) } { eclair.nodes(Some(Set(remoteNodeAnn1.nodeId, remoteNodeAnn2.nodeId))).pipeTo(sender.ref) router.expectMsg(Router.GetNodes) router.reply(allNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet === Set(remoteNodeAnn1, remoteNodeAnn2)) + assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet == Set(remoteNodeAnn1, remoteNodeAnn2)) } { eclair.nodes(Some(Set(randomKey().publicKey))).pipeTo(sender.ref) @@ -208,11 +216,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I ) val (a, b, c, d, e) = (a_priv.publicKey, b_priv.publicKey, c_priv.publicKey, d_priv.publicKey, e_priv.publicKey) - val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) assert(Announcements.isNode1(a, b)) assert(Announcements.isNode1(b, c)) @@ -231,7 +239,26 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.allUpdates(Some(b)).pipeTo(sender.ref) // ask updates filtered by 'b' router.expectMsg(Router.GetChannelsMap) router.reply(channels) - assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet === Set(ShortChannelId(2))) + assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet == Set(ShortChannelId(2))) + } + + test("open with bad arguments") { f => + import f._ + + val eclair = new EclairImpl(kit) + + // option_scid_alias is not compatible with public channels + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + assert(sender.expectMsgType[Status.Failure].cause.getMessage.contains("option_scid_alias is not compatible with public channels")) + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] } test("close and forceclose should work both with channelId and shortChannelId") { f => @@ -240,35 +267,35 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) eclair.forceClose(Left(ByteVector32.Zeroes) :: Nil) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) - eclair.forceClose(Right(ShortChannelId("568749x2597x0")) :: Nil) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_FORCECLOSE(ActorRef.noSender))) + eclair.forceClose(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender))) - eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId("568749x2597x0")) :: Nil) + eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_FORCECLOSE(ActorRef.noSender)) + Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender)) ) eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, None) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, None))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, None))) val customClosingFees = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(200 sat), FeeratePerKw(1000 sat)) eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, Some(customClosingFees)) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, None, None) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None))) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, None, None) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Left(ByteVector32.One) :: Right(ShortChannelId("568749x2597x1")) :: Nil, None, None) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Left(ByteVector32.One) :: Right(ShortChannelId.fromCoordinates("568749x2597x1").success.value) :: Nil, None, None) register.expectMsgAllOf( - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None)), - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x1"), CMD_CLOSE(ActorRef.noSender, None, None)) + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None)), + Register.Forward(null, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x1").success.value, CMD_CLOSE(ActorRef.noSender, None, None)) ) } @@ -280,9 +307,9 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] - assert(receive.amount_opt === Some(123 msat)) - assert(receive.expirySeconds_opt === Some(456)) - assert(receive.fallbackAddress_opt === Some(fallBackAddressRaw)) + assert(receive.amount_opt.contains(123 msat)) + assert(receive.expirySeconds_opt.contains(456)) + assert(receive.fallbackAddress_opt.contains(fallBackAddressRaw)) // try with wrong address format assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None)) @@ -296,7 +323,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val paymentPreimage = randomBytes32() eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage)).pipeTo(sender.ref) - assert(sender.expectMsgType[PaymentRequest].paymentHash === Crypto.sha256(paymentPreimage)) + assert(sender.expectMsgType[Invoice].paymentHash == Crypto.sha256(paymentPreimage)) } test("sendtoroute should pass the parameters correctly") { f => @@ -307,9 +334,9 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val trampolines = Seq(randomKey().publicKey, randomKey().publicKey) val parentId = UUID.randomUUID() val secret = randomBytes32() - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) - eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) - paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, CltvExpiryDelta(123), route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) + eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) + paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) } test("call sendWithPreimage, which generates a random preimage, to perform a KeySend payment") { f => @@ -320,10 +347,10 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId === None) - assert(send.recipientNodeId === nodeId) - assert(send.recipientAmount === 12345.msat) - assert(send.paymentHash === Crypto.sha256(send.paymentPreimage)) + assert(send.externalId.isEmpty) + assert(send.recipientNodeId == nodeId) + assert(send.recipientAmount == 12345.msat) + assert(send.paymentHash == Crypto.sha256(send.paymentPreimage)) assert(send.userCustomTlvs.isEmpty) } @@ -337,11 +364,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat, paymentPreimage = expectedPaymentPreimage) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId === None) - assert(send.recipientNodeId === nodeId) - assert(send.recipientAmount === 12345.msat) - assert(send.paymentPreimage === expectedPaymentPreimage) - assert(send.paymentHash === expectedPaymentHash) + assert(send.externalId.isEmpty) + assert(send.recipientNodeId == nodeId) + assert(send.recipientAmount == 12345.msat) + assert(send.paymentPreimage == expectedPaymentPreimage) + assert(send.paymentHash == expectedPaymentHash) assert(send.userCustomTlvs.isEmpty) } @@ -354,17 +381,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val verifiedMessage = eclair.verifyMessage(bytesMsg, signedMessage.signature) assert(verifiedMessage.valid) - assert(verifiedMessage.publicKey === kit.nodeParams.nodeId) + assert(verifiedMessage.publicKey == kit.nodeParams.nodeId) val prefix = ByteVector("Lightning Signed Message:".getBytes) val dhash256 = Crypto.hash256(prefix ++ bytesMsg) val expectedDigest = ByteVector32(hex"cbedbc1542fb139e2e10954f1ff9f82e8a1031cc63260636bbc45a90114552ea") - assert(dhash256 === expectedDigest) + assert(dhash256 == expectedDigest) assert(Crypto.verifySignature(dhash256, ByteVector64(signedMessage.signature.tail), kit.nodeParams.nodeId)) } @@ -377,8 +404,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val wrongMsg = ByteVector.fromValidBase64(base64Msg.tail) val verifiedMessage = eclair.verifyMessage(wrongMsg, signedMessage.signature) @@ -405,8 +432,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val invalidSignature = (if (signedMessage.signature.head.toInt == 31) 32 else 31).toByte +: signedMessage.signature.tail val verifiedMessage = eclair.verifyMessage(bytesMsg, invalidSignature) @@ -430,17 +457,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.expectMsg(Symbol("channels")) register.reply(map) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(map(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) - register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_FAILURE(CMD_GETINFO(ActorRef.noSender), new IllegalArgumentException("Non-standard channel"))) - val c3 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(map(c3.channelId), c3.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(map(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_FAILURE(CMD_GET_CHANNEL_INFO(ActorRef.noSender), new IllegalArgumentException("Non-standard channel"))) + val c3 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(map(c3.channelId), c3.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GETINFO]].toSet === Set( - RES_GETINFO(a, a1, NORMAL, ChannelCodecsSpec.normal), - RES_GETINFO(b, b1, NORMAL, ChannelCodecsSpec.normal), + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet == Set( + RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), + RES_GET_CHANNEL_INFO(b, b1, NORMAL, ChannelCodecsSpec.normal), )) } @@ -461,15 +488,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.expectMsg(Symbol("channelsTo")) register.reply(channels2Nodes) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) - val c2 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c2.channelId), c2.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c2 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c2.channelId), c2.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GETINFO]].toSet === Set( - RES_GETINFO(a, a1, NORMAL, ChannelCodecsSpec.normal), - RES_GETINFO(a, a2, NORMAL, ChannelCodecsSpec.normal), + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet == Set( + RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), + RES_GET_CHANNEL_INFO(a, a2, NORMAL, ChannelCodecsSpec.normal), )) } @@ -487,11 +514,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.channelInfo(Left(a2)).pipeTo(sender.ref) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - sender.expectMsg(RES_GETINFO(a, a2, NORMAL, ChannelCodecsSpec.normal)) + sender.expectMsg(RES_GET_CHANNEL_INFO(a, a2, NORMAL, ChannelCodecsSpec.normal)) } test("get sent payment info") { f => @@ -520,11 +547,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I paymentInitiator.expectMsg(GetPayment(Right(spontaneousPayment.paymentHash))) paymentInitiator.reply(PaymentIsPending(pendingPaymentId, spontaneousPayment.paymentHash, PendingSpontaneousPayment(ActorRef.noSender, spontaneousPayment))) val pendingPayment2 = sender.expectMsgType[Seq[OutgoingPayment]] - assert(pendingPayment2.length === 1) - assert(pendingPayment2.head.id === pendingPayment2.head.parentId) - assert(pendingPayment2.head.id === pendingPaymentId) - assert(pendingPayment2.head.paymentHash === spontaneousPayment.paymentHash) - assert(pendingPayment2.head.status === OutgoingPaymentStatus.Pending) + assert(pendingPayment2.length == 1) + assert(pendingPayment2.head.id == pendingPayment2.head.parentId) + assert(pendingPayment2.head.id == pendingPaymentId) + assert(pendingPayment2.head.paymentHash == spontaneousPayment.paymentHash) + assert(pendingPayment2.head.status == OutgoingPaymentStatus.Pending) // A third payment is fully settled in the DB and not being retried. val failedAt = TimestampMilli.now() @@ -541,11 +568,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I paymentInitiator.expectMsg(GetPayment(Left(failedPayment.parentId))) paymentInitiator.reply(PaymentIsPending(failedPayment.parentId, spontaneousPayment.paymentHash, PendingSpontaneousPayment(ActorRef.noSender, spontaneousPayment))) val pendingPayment3 = sender.expectMsgType[Seq[OutgoingPayment]] - assert(pendingPayment3.length === 2) - assert(pendingPayment3.head.id === failedPayment.parentId) - assert(pendingPayment3.head.paymentHash === failedPayment.paymentHash) - assert(pendingPayment3.head.status === OutgoingPaymentStatus.Pending) - assert(pendingPayment3.last === failedPayment) + assert(pendingPayment3.length == 2) + assert(pendingPayment3.head.id == failedPayment.parentId) + assert(pendingPayment3.head.paymentHash == failedPayment.paymentHash) + assert(pendingPayment3.head.status == OutgoingPaymentStatus.Pending) + assert(pendingPayment3.last == failedPayment) } test("close channels") { f => @@ -564,7 +591,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(RES_SUCCESS(c2.message, c2.channelId)) register.expectNoMessage() - assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] === Map( + assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] == Map( Left(a) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), a)), Left(b) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), b)) )) @@ -601,7 +628,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(Register.ForwardFailure(u3)) register.expectNoMessage() - assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] === Map( + assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] == Map( Left(a1) -> Right(RES_SUCCESS(CMD_UPDATE_RELAY_FEE(ActorRef.noSender, 999 msat, 1234, None), a1)), Left(a2) -> Right(RES_FAILURE(CMD_UPDATE_RELAY_FEE(ActorRef.noSender, 999 msat, 1234, None), CommandUnavailableInThisState(a2, "CMD_UPDATE_RELAY_FEE", channel.CLOSING))), Left(b1) -> Left(ChannelNotFound(Left(b1))) @@ -611,4 +638,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I peersDb.addOrUpdateRelayFees(b, RelayFees(999 msat, 1234)).wasCalled(once) } + test("channelBalances asks for all channels, usableBalances only for enabled ones") { f => + import f._ + + val eclair = new EclairImpl(kit) + + eclair.channelBalances().pipeTo(sender.ref) + relayer.expectMsg(GetOutgoingChannels(enabledOnly = false)) + eclair.usableBalances().pipeTo(sender.ref) + relayer.expectMsg(GetOutgoingChannels()) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 0e2a700b1c..1bf74af7a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -62,45 +62,45 @@ class FeaturesSpec extends AnyFunSuite { test("features dependencies") { val testCases = Map( - bin" " -> true, - bin" 00000000" -> true, - bin" 01011000" -> true, + bin" " -> true, + bin" 00000000" -> true, + bin" 01011000" -> true, // gossip_queries_ex depend on gossip_queries - bin"000000000000100000000000" -> false, - bin"000000000000010000000000" -> false, - bin"000000000000100010000000" -> true, - bin"000000000000100001000000" -> true, - // payment_secret depends on var_onion_optin, but we allow not setting it to be compatible with Phoenix - bin"000000001000000000000000" -> true, - bin"000000000100000000000000" -> true, - bin"000000000100001000000000" -> true, + bin" 000000000000100000000000" -> false, + bin" 000000000000010000000000" -> false, + bin" 000000000000100010000000" -> true, + bin" 000000000000100001000000" -> true, + // payment_secret depends on var_onion_optin + bin" 000000001000000000000000" -> false, + bin" 000000000100000000000000" -> false, + bin" 000000000100001000000000" -> true, // basic_mpp depends on payment_secret - bin"000000100000000000000000" -> false, - bin"000000010000000000000000" -> false, - bin"000000101000000000000000" -> true, // we allow not setting var_onion_optin - bin"000000011000000000000000" -> true, // we allow not setting var_onion_optin - bin"000000011000001000000000" -> true, - bin"000000100100000100000000" -> true, + bin" 000000100000000000000000" -> false, + bin" 000000010000000000000000" -> false, + bin" 000000101000000100000000" -> true, + bin" 000000011000000100000000" -> true, + bin" 000000011000001000000000" -> true, + bin" 000000100100000100000000" -> true, // option_anchor_outputs depends on option_static_remotekey - bin"001000000000000000000000" -> false, - bin"000100000000000000000000" -> false, - bin"001000000010000000000000" -> true, - bin"001000000001000000000000" -> true, - bin"000100000010000000000000" -> true, - bin"000100000001000000000000" -> true, + bin" 001000000000000000000000" -> false, + bin" 000100000000000000000000" -> false, + bin" 001000000010000000000000" -> true, + bin" 001000000001000000000000" -> true, + bin" 000100000010000000000000" -> true, + bin" 000100000001000000000000" -> true, // option_anchors_zero_fee_htlc_tx depends on option_static_remotekey - bin"100000000000000000000000" -> false, - bin"010000000000000000000000" -> false, - bin"100000000010000000000000" -> true, - bin"100000000001000000000000" -> true, - bin"010000000010000000000000" -> true, - bin"010000000001000000000000" -> true, + bin" 100000000000000000000000" -> false, + bin" 010000000000000000000000" -> false, + bin" 100000000010000000000000" -> true, + bin" 100000000001000000000000" -> true, + bin" 010000000010000000000000" -> true, + bin" 010000000001000000000000" -> true, ) for ((testCase, valid) <- testCases) { if (valid) { - assert(validateFeatureGraph(Features(testCase)) === None) - assert(validateFeatureGraph(Features(testCase.bytes)) === None) + assert(validateFeatureGraph(Features(testCase)) == None) + assert(validateFeatureGraph(Features(testCase.bytes)) == None) } else { assert(validateFeatureGraph(Features(testCase)).nonEmpty) assert(validateFeatureGraph(Features(testCase.bytes)).nonEmpty) @@ -109,7 +109,7 @@ class FeaturesSpec extends AnyFunSuite { } test("features compatibility") { - case class TestCase(ours: Features, theirs: Features, oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) + case class TestCase(ours: Features[Feature], theirs: Features[Feature], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) val testCases = Seq( // Empty features TestCase( @@ -168,7 +168,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown optional features TestCase( Features(VariableLengthOnion -> Optional), - Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))), + Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(141))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true @@ -176,7 +176,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown mandatory features TestCase( Features(VariableLengthOnion -> Optional), - Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))), + Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(142))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false @@ -198,33 +198,36 @@ class FeaturesSpec extends AnyFunSuite { compatible = false ), // nonreg testing of future features (needs to be updated with every new supported mandatory bit) - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), ) for (testCase <- testCases) { - assert(areCompatible(testCase.ours, testCase.theirs) === testCase.compatible, testCase) - assert(testCase.ours.areSupported(testCase.theirs) === testCase.oursSupportTheirs, testCase) - assert(testCase.theirs.areSupported(testCase.ours) === testCase.theirsSupportOurs, testCase) + assert(areCompatible(testCase.ours, testCase.theirs) == testCase.compatible, testCase) + assert(testCase.ours.areSupported(testCase.theirs) == testCase.oursSupportTheirs, testCase) + assert(testCase.theirs.areSupported(testCase.ours) == testCase.theirsSupportOurs, testCase) } } test("filter features based on their usage") { val features = Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), - Set(UnknownFeature(753), UnknownFeature(852)) + Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) ) - assert(features.initFeatures() === Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), - Set(UnknownFeature(753), UnknownFeature(852)) + assert(features.initFeatures() == Features( + Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.nodeAnnouncementFeatures() === Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), - Set(UnknownFeature(753), UnknownFeature(852)) + assert(features.nodeAnnouncementFeatures() == Features( + Map(DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) + )) + assert(features.invoiceFeatures() == Features( + Map(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.invoiceFeatures() === Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional)) } test("features to bytes") { @@ -232,16 +235,16 @@ class FeaturesSpec extends AnyFunSuite { hex"" -> Features.empty, hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), - hex"09004200" -> Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), - hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), + hex"80010080000000000000000000000000000000000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(151), UnknownFeature(160), UnknownFeature(175))) ) for ((bin, features) <- testCases) { - assert(features.toByteVector === bin) - assert(Features(bin) === features) + assert(features.toByteVector == bin) + assert(Features(bin) == features) val notMinimallyEncoded = Features(hex"00" ++ bin) - assert(notMinimallyEncoded === features) - assert(notMinimallyEncoded.toByteVector === bin) // features are minimally-encoded when converting to bytes + assert(notMinimallyEncoded == features) + assert(notMinimallyEncoded.toByteVector == bin) // features are minimally-encoded when converting to bytes } } @@ -249,21 +252,19 @@ class FeaturesSpec extends AnyFunSuite { { val conf = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = optional - | initial_routing_sync = optional - | gossip_queries = optional - | gossip_queries_ex = optional - | var_onion_optin = optional - | payment_secret = optional - | basic_mpp = optional - |} - """.stripMargin) + option_data_loss_protect = optional + initial_routing_sync = optional + gossip_queries = optional + gossip_queries_ex = optional + var_onion_optin = optional + payment_secret = optional + basic_mpp = optional + """) val features = fromConfiguration(conf) - assert(features.toByteVector === hex"028a8a") - assert(Features(hex"028a8a") === features) - assert(validateFeatureGraph(features) === None) + assert(features.toByteVector == hex"028a8a") + assert(Features(hex"028a8a") == features) + assert(validateFeatureGraph(features) == None) assert(features.hasFeature(DataLossProtect, Some(Optional))) assert(features.hasFeature(InitialRoutingSync, Some(Optional))) assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) @@ -276,21 +277,17 @@ class FeaturesSpec extends AnyFunSuite { { val conf = ConfigFactory.parseString( """ - | features { - | initial_routing_sync = optional - | option_data_loss_protect = optional - | gossip_queries = optional - | gossip_queries_ex = mandatory - | var_onion_optin = optional - | } - | - """.stripMargin - ) + initial_routing_sync = optional + option_data_loss_protect = optional + gossip_queries = optional + gossip_queries_ex = mandatory + var_onion_optin = optional + """) val features = fromConfiguration(conf) - assert(features.toByteVector === hex"068a") - assert(Features(hex"068a") === features) - assert(validateFeatureGraph(features) === None) + assert(features.toByteVector == hex"068a") + assert(Features(hex"068a") == features) + assert(validateFeatureGraph(features) == None) assert(features.hasFeature(DataLossProtect, Some(Optional))) assert(features.hasFeature(InitialRoutingSync, Some(Optional))) assert(!features.hasFeature(InitialRoutingSync, Some(Mandatory))) @@ -303,29 +300,21 @@ class FeaturesSpec extends AnyFunSuite { { val confWithUnknownFeatures = ConfigFactory.parseString( """ - |features { - | option_non_existent = mandatory # this is ignored - | gossip_queries = optional - | payment_secret = mandatory - |} - """.stripMargin) + option_non_existent = mandatory + gossip_queries = optional + payment_secret = mandatory + """) - val features = fromConfiguration(confWithUnknownFeatures) - assert(features.toByteVector === hex"4080") - assert(Features(hex"4080") === features) - assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) - assert(features.hasFeature(PaymentSecret, Some(Mandatory))) + assertThrows[RuntimeException](fromConfiguration(confWithUnknownFeatures)) } { val confWithUnknownSupport = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = what - | gossip_queries = optional - | payment_secret = mandatory - |} - """.stripMargin) + option_data_loss_protect = what + gossip_queries = optional + payment_secret = mandatory + """) assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport)) } @@ -333,14 +322,12 @@ class FeaturesSpec extends AnyFunSuite { { val confWithDisabledFeatures = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = disabled - | gossip_queries = optional - | payment_secret = mandatory - | option_support_large_channel = disabled - | gossip_queries_ex = mandatory - |} - """.stripMargin) + option_data_loss_protect = disabled + gossip_queries = optional + payment_secret = mandatory + option_support_large_channel = disabled + gossip_queries_ex = mandatory + """) val features = fromConfiguration(confWithDisabledFeatures) assert(!features.hasFeature(DataLossProtect)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala index 89c50e5266..6b2133becc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import org.scalatest.funsuite.AnyFunSuite /** @@ -27,26 +27,26 @@ class MilliSatoshiSpec extends AnyFunSuite { test("millisatoshi numeric operations") { // add - assert(MilliSatoshi(561) + 0.msat === MilliSatoshi(561)) - assert(MilliSatoshi(561) + 0.sat === MilliSatoshi(561)) - assert(MilliSatoshi(561) + 1105.msat === MilliSatoshi(1666)) - assert(MilliSatoshi(2000) + 3.sat === MilliSatoshi(5000)) + assert(MilliSatoshi(561) + 0.msat == MilliSatoshi(561)) + assert(MilliSatoshi(561) + 0.sat == MilliSatoshi(561)) + assert(MilliSatoshi(561) + 1105.msat == MilliSatoshi(1666)) + assert(MilliSatoshi(2000) + 3.sat == MilliSatoshi(5000)) // subtract - assert(MilliSatoshi(561) - 0.msat === MilliSatoshi(561)) - assert(MilliSatoshi(1105) - 561.msat === MilliSatoshi(544)) - assert(561.msat - 1105.msat === -MilliSatoshi(544)) - assert(MilliSatoshi(561) - 1105.msat === -MilliSatoshi(544)) - assert(MilliSatoshi(1105) - 1.sat === MilliSatoshi(105)) + assert(MilliSatoshi(561) - 0.msat == MilliSatoshi(561)) + assert(MilliSatoshi(1105) - 561.msat == MilliSatoshi(544)) + assert(561.msat - 1105.msat == -MilliSatoshi(544)) + assert(MilliSatoshi(561) - 1105.msat == -MilliSatoshi(544)) + assert(MilliSatoshi(1105) - 1.sat == MilliSatoshi(105)) // multiply - assert(MilliSatoshi(561) * 1 === 561.msat) - assert(MilliSatoshi(561) * 2 === 1122.msat) - assert(MilliSatoshi(561) * 2.5 === 1402.msat) + assert(MilliSatoshi(561) * 1 == 561.msat) + assert(MilliSatoshi(561) * 2 == 1122.msat) + assert(MilliSatoshi(561) * 2.5 == 1402.msat) // divide - assert(MilliSatoshi(561) / 1 === MilliSatoshi(561)) - assert(MilliSatoshi(561) / 2 === MilliSatoshi(280)) + assert(MilliSatoshi(561) / 1 == MilliSatoshi(561)) + assert(MilliSatoshi(561) / 2 == MilliSatoshi(280)) // compare assert(MilliSatoshi(561) <= MilliSatoshi(561)) @@ -63,16 +63,16 @@ class MilliSatoshiSpec extends AnyFunSuite { assert(MilliSatoshi(2000) > Satoshi(1)) // maxOf - assert((561 msat).max(1105 msat) === MilliSatoshi(1105)) - assert((1105 msat).max(1 sat) === MilliSatoshi(1105)) - assert((1105 msat).max(2 sat) === MilliSatoshi(2000)) - assert((1 sat).max(2 sat) === Satoshi(2)) + assert((561 msat).max(1105 msat) == MilliSatoshi(1105)) + assert((1105 msat).max(1 sat) == MilliSatoshi(1105)) + assert((1105 msat).max(2 sat) == MilliSatoshi(2000)) + assert((1 sat).max(2 sat) == Satoshi(2)) // minOf - assert((561 msat).min(1105 msat) === MilliSatoshi(561)) - assert((1105 msat).min(1 sat) === MilliSatoshi(1000)) - assert((1105 msat).min(2 sat) === MilliSatoshi(1105)) - assert((1 sat).min(2 sat) === Satoshi(1)) + assert((561 msat).min(1105 msat) == MilliSatoshi(561)) + assert((1105 msat).min(1 sat) == MilliSatoshi(1000)) + assert((1105 msat).min(2 sat) == MilliSatoshi(1105)) + assert((1 sat).min(2 sat) == Satoshi(1)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala index fb5c8960a2..9427149101 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -16,8 +16,10 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, Script} +import com.google.common.net.InetAddresses +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Script} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -28,6 +30,9 @@ import scala.util.Try */ class PackageSpec extends AnyFunSuite { + implicit def byteVector2array(input: ByteVector): Array[Byte] = input.toArray + + implicit def byteVector322array(input: ByteVector32): Array[Byte] = input.toArray test("compute long channel id") { val data = ((hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") :: @@ -36,7 +41,7 @@ class PackageSpec extends AnyFunSuite { (hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00F0", 0x0F00, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FF0") :: Nil) .map(x => (ByteVector32(x._1), x._2, ByteVector32(x._3))) - data.foreach(x => assert(toLongId(ByteVector32(x._1), x._2) === x._3)) + data.foreach(x => assert(toLongId(ByteVector32(x._1), x._2) == x._3)) } test("decode base58 addresses") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index e26254dddf..094f4dfa4b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -18,43 +18,63 @@ package fr.acinq.eclair import org.scalatest.funsuite.AnyFunSuite -import scala.util.Try - - +import scala.util.Success class ShortChannelIdSpec extends AnyFunSuite { - test("handle values from 0 to 0xffffffffffff") { + test("handle real short channel ids from 0 to 0xffffffffffff") { val expected = Map( - TxCoordinates(BlockHeight(0), 0, 0) -> ShortChannelId(0), - TxCoordinates(BlockHeight(42000), 27, 3) -> ShortChannelId(0x0000a41000001b0003L), - TxCoordinates(BlockHeight(1258612), 63, 0) -> ShortChannelId(0x13347400003f0000L), - TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> ShortChannelId(0xffffff000000ffffL), - TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> ShortChannelId(0x000000ffffffffffL), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> ShortChannelId(0xffffffffffff0000L), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> ShortChannelId(0xffffffffffffffffL) + TxCoordinates(BlockHeight(0), 0, 0) -> RealShortChannelId(0), + TxCoordinates(BlockHeight(42000), 27, 3) -> RealShortChannelId(0x0000a41000001b0003L), + TxCoordinates(BlockHeight(1258612), 63, 0) -> RealShortChannelId(0x13347400003f0000L), + TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> RealShortChannelId(0xffffff000000ffffL), + TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> RealShortChannelId(0x000000ffffffffffL), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> RealShortChannelId(0xffffffffffff0000L), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> RealShortChannelId(0xffffffffffffffffL) ) for ((coord, shortChannelId) <- expected) { - assert(shortChannelId == ShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) + assert(shortChannelId == RealShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) assert(coord == ShortChannelId.coordinates(shortChannelId)) } } test("human readable format as per spec") { - assert(ShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") + assert(RealShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") } - test("parse a short channel it") { - assert(ShortChannelId("42000x27x3").toLong == 0x0000a41000001b0003L) + test("parse a short channel id") { + assert(ShortChannelId.fromCoordinates("42000x27x3").map(_.toLong) == Success(0x0000a41000001b0003L)) } test("fail parsing a short channel id if not in the required form") { - assert(Try(ShortChannelId("42000x27x3.1")).isFailure) - assert(Try(ShortChannelId("4200aa0x27x3")).isFailure) - assert(Try(ShortChannelId("4200027x3")).isFailure) - assert(Try(ShortChannelId("42000x27ax3")).isFailure) - assert(Try(ShortChannelId("42000x27x")).isFailure) - assert(Try(ShortChannelId("42000x27")).isFailure) - assert(Try(ShortChannelId("42000x")).isFailure) + assert(ShortChannelId.fromCoordinates("42000x27x3.1").isFailure) + assert(ShortChannelId.fromCoordinates("4200aa0x27x3").isFailure) + assert(ShortChannelId.fromCoordinates("4200027x3").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27ax3").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27x").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27").isFailure) + assert(ShortChannelId.fromCoordinates("42000x").isFailure) + } + + test("compare different types of short channel ids") { + val id = 123456 + val alias = Alias(id) + val realScid = RealShortChannelId(id) + val scid = ShortChannelId(id) + assert(alias == realScid) + assert(realScid == scid) + val m = Map(alias -> "alias", realScid -> "real", scid -> "unknown") + // all scids are in the same key space + assert(m.size == 1) + // Values outside of the range [0;0xffffffffffff] can be used for aliases. + Seq(-561L, 0xffffffffffffffffL, 0x2affffffffffffffL).foreach(id => assert(Alias(id) == UnspecifiedShortChannelId(id))) } -} \ No newline at end of file + + test("basic check on random alias generation") { + for(_ <- 0 until 100) { + val alias = ShortChannelId.generateLocalAlias() + assert(alias.toLong >= 0 && alias.toLong <= 384_829_069_721_665_536L) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index a66c500730..c0779a845e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee.{DustTolerance, FeeratePerByte, FeeratePerKw, FeerateTolerance} @@ -33,7 +33,7 @@ import scala.util.Try class StartupSpec extends AnyFunSuite { - val defaultConf = ConfigFactory.load("reference.conf").getConfig("eclair") + val defaultConf: Config = ConfigFactory.load("reference.conf").getConfig("eclair") def makeNodeParamsWithDefaults(conf: Config): NodeParams = { val blockCount = new AtomicLong(0) @@ -54,14 +54,14 @@ class StartupSpec extends AnyFunSuite { val threeBytesUTFChar = '\u20AC' // € val baseUkraineAlias = "BitcoinLightningNodeUkraine" - assert(baseUkraineAlias.length === 27) - assert(baseUkraineAlias.getBytes.length === 27) + assert(baseUkraineAlias.length == 27) + assert(baseUkraineAlias.getBytes.length == 27) // we add 2 UTF-8 chars, each is 3-bytes long -> total new length 33 bytes! val goUkraineGo = s"${threeBytesUTFChar}BitcoinLightningNodeUkraine$threeBytesUTFChar" - assert(goUkraineGo.length === 29) - assert(goUkraineGo.getBytes.length === 33) // too long for the alias, should be truncated + assert(goUkraineGo.length == 29) + assert(goUkraineGo.getBytes.length == 33) // too long for the alias, should be truncated val illegalAliasConf = ConfigFactory.parseString(s"node-alias = $goUkraineGo") val conf = illegalAliasConf.withFallback(defaultConf) @@ -166,60 +166,122 @@ class StartupSpec extends AnyFunSuite { test("parse human readable override features") { val perNodeConf = ConfigFactory.parseString( """ - | override-features = [ // optional per-node features + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | } + | override-init-features = [ // optional per-node features | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | basic_mpp = mandatory - | option_channel_type = optional - | } + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | basic_mpp = mandatory + | gossip_queries = optional + | } | } | ] """.stripMargin ) - val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) - assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf.withoutPath("features"))) + val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) + assert(perNodeFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelRangeQueries -> Optional, ChannelType -> Optional)) } - test("filter out non-init features in node override") { + test("combine node override features with default features") { val perNodeConf = ConfigFactory.parseString( """ - | override-features = [ // optional per-node features + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | basic_mpp = mandatory + | option_static_remotekey = optional + | option_channel_type = optional + | } + | override-init-features = [ // optional per-node features | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | option_channel_type = optional - | option_payment_metadata = disabled - | } + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | option_static_remotekey = mandatory + | option_anchors_zero_fee_htlc_tx = optional + | } | }, | { - | nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | option_channel_type = optional - | option_payment_metadata = mandatory - | } + | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + | features { + | basic_mpp = optional + | option_static_remotekey = disabled + | } + | } + | ] + """.stripMargin + ) + + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf.withoutPath("features"))) + val defaultFeatures = nodeParams.features + assert(defaultFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, StaticRemoteKey -> Optional, ChannelType -> Optional)) + val perNodeFeatures1 = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) + assert(perNodeFeatures1 == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional, ChannelType -> Optional)) + val perNodeFeatures2 = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"))) + assert(perNodeFeatures2 == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ChannelType -> Optional)) + } + + test("reject non-init features in node override") { + val perNodeConf = ConfigFactory.parseString( + """ + | override-init-features = [ // optional per-node features + | { + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | option_payment_metadata = disabled + | } + | }, + | { + | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | option_payment_metadata = mandatory + | } + | } + | ] + """.stripMargin + ) + + assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))) + } + + test("disallow enabling zero-conf for every peer") { + val invalidConf = ConfigFactory.parseString( + """ + | features { + | option_zeroconf = optional + | } + """.stripMargin + ) + assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(invalidConf.withFallback(defaultConf))) + + val perNodeConf = ConfigFactory.parseString( + """ + | override-init-features = [ + | { + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | option_zeroconf = optional + | } | } | ] """.stripMargin ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - val perNodeFeaturesA = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) - val perNodeFeaturesB = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))) - val defaultNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"))) - // Some features should never be sent in init messages. - assert(nodeParams.features.hasFeature(PaymentMetadata)) - assert(!perNodeFeaturesA.hasFeature(PaymentMetadata)) - assert(!perNodeFeaturesB.hasFeature(PaymentMetadata)) - assert(!defaultNodeFeatures.hasFeature(PaymentMetadata)) + assert(!nodeParams.features.hasFeature(Features.ZeroConf)) + val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) + assert(perNodeFeatures.hasFeature(Features.ZeroConf)) } test("override feerate mismatch tolerance") { @@ -227,7 +289,7 @@ class StartupSpec extends AnyFunSuite { """ | on-chain-fees.override-feerate-tolerance = [ | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" | feerate-tolerance { | ratio-low = 0.1 | ratio-high = 15.0 @@ -239,7 +301,7 @@ class StartupSpec extends AnyFunSuite { | } | }, | { - | nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + | nodeid = "03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b" | feerate-tolerance { | ratio-low = 0.75 | ratio-high = 5.0 @@ -255,14 +317,30 @@ class StartupSpec extends AnyFunSuite { ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) === FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) === FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) === FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) == FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) == FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) == FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) } test("NodeParams should fail if htlc-minimum-msat is set to 0") { - val noHtlcMinimumConf = ConfigFactory.parseString("htlc-minimum-msat = 0") + val noHtlcMinimumConf = ConfigFactory.parseString("channel.htlc-minimum-msat = 0") assert(Try(makeNodeParamsWithDefaults(noHtlcMinimumConf.withFallback(defaultConf))).isFailure) } + test("NodeParams should fail with deprecated channel.min-funding-satoshis set") { + val illegalGlobalFeaturesConf = ConfigFactory.parseString("channel.min-funding-satoshis = 200000") + val illegalConf = illegalGlobalFeaturesConf.withFallback(defaultConf) + + val nodeParamsAttempt1 = Try(makeNodeParamsWithDefaults(illegalConf)) + assert(nodeParamsAttempt1.isFailure && nodeParamsAttempt1.failed.get.getMessage.contains("channel.min-public-funding-satoshis, channel.min-private-funding-satoshis")) + + val newGlobalFeaturesConf = ConfigFactory.parseMap(Map( + s"channel.min-public-funding-satoshis" -> "20000", + s"channel.min-private-funding-satoshis" -> "20000", + ).asJava) + val legalConf = newGlobalFeaturesConf.withFallback(defaultConf) + val nodeParamsAttempt2 = Try(makeNodeParamsWithDefaults(legalConf)) + assert(nodeParamsAttempt2.isSuccess) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala index ef69920b87..25d7ee41bd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair import akka.actor.ActorSystem -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient} @@ -28,7 +28,7 @@ import scala.concurrent.{ExecutionContext, Future} /** * Created by PM on 26/04/2016. */ -class TestBitcoinCoreClient()(implicit system: ActorSystem) extends BitcoinCoreClient(new BasicBitcoinJsonRPCClient(UserPassword("", ""), "", 0)(http = null)) { +class TestBitcoinCoreClient()(implicit system: ActorSystem) extends BitcoinCoreClient(new BasicBitcoinJsonRPCClient(UserPassword("", ""), "", 0)(sb = null)) { import scala.concurrent.ExecutionContext.Implicits.global diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 55f31f0716..7a374fa10b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -16,12 +16,12 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy -import fr.acinq.eclair.channel.LocalParams +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy} +import fr.acinq.eclair.channel.{ChannelFlags, LocalParams} import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.MessageRelay.RelayAll import fr.acinq.eclair.io.{Peer, PeerConnection} @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.PathFindingExperimentConf import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress, OnionRoutingPacket} import org.scalatest.Tag -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID import java.util.concurrent.atomic.AtomicLong @@ -71,7 +71,7 @@ object TestConstants { ) object Alice { - val seed: ByteVector32 = ByteVector32(ByteVector.fill(32)(1)) + val seed: ByteVector32 = ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3") // 02aaaa... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) @@ -85,7 +85,7 @@ object TestConstants { publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, torAddress_opt = None, features = Features( - Map[Feature, FeatureSupport]( + Map( DataLossProtect -> Optional, ChannelRangeQueries -> Optional, ChannelRangeQueriesExtended -> Optional, @@ -94,32 +94,43 @@ object TestConstants { BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, ), - Set(UnknownFeature(TestFeature.optional)) + unknown = Set(UnknownFeature(TestFeature.optional)) ), pluginParams = List(pluginParams), - overrideFeatures = Map.empty, + overrideInitFeatures = Map.empty, syncWhitelist = Set.empty, - dustLimit = 1100 sat, - maxRemoteDustLimit = 1500 sat, + channelConf = ChannelConf( + dustLimit = 1100 sat, + maxRemoteDustLimit = 1500 sat, + maxHtlcValueInFlightMsat = UInt64(500000000), + maxAcceptedHtlcs = 100, + expiryDelta = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), + minFinalExpiryDelta = CltvExpiryDelta(18), + maxBlockProcessingDelay = 10 millis, + maxTxPublishRetryDelay = 10 millis, + htlcMinimum = 0 msat, + minDepthBlocks = 3, + toRemoteDelay = CltvExpiryDelta(144), + maxToLocalDelay = CltvExpiryDelta(1000), + reserveToFundingRatio = 0.01, // note: not used (overridden below) + maxReserveToFundingRatio = 0.05, + unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + revocationTimeout = 20 seconds, + channelFlags = ChannelFlags.Public, + minFundingPublicSatoshis = 1000 sat, + minFundingPrivateSatoshis = 900 sat, + maxFundingSatoshis = 16777215 sat, + ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true)), perNodeFeerateTolerance = Map.empty ), - maxHtlcValueInFlightMsat = UInt64(500000000), - maxAcceptedHtlcs = 100, - expiryDelta = CltvExpiryDelta(144), - fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), - minFinalExpiryDelta = CltvExpiryDelta(18), - maxBlockProcessingDelay = 10 millis, - maxTxPublishRetryDelay = 10 millis, - htlcMinimum = 0 msat, - minDepthBlocks = 3, - toRemoteDelay = CltvExpiryDelta(144), - maxToLocalDelay = CltvExpiryDelta(1000), relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, @@ -129,22 +140,15 @@ object TestConstants { feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionths = 30)), - reserveToFundingRatio = 0.01, // note: not used (overridden below) - maxReserveToFundingRatio = 0.05, - unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + feeProportionalMillionths = 30), + enforcementDelay = 10 minutes), db = TestDatabases.inMemoryDb(), - revocationTimeout = 20 seconds, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1, - watchSpentWindow = 1 second, - paymentRequestExpiry = 1 hour, + invoiceExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, - minFundingSatoshis = 1000 sat, - maxFundingSatoshis = 16777215 sat, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, initTimeout = 10 seconds, @@ -153,11 +157,13 @@ object TestConstants { pingDisconnect = true, maxRebroadcastDelay = 5 seconds, killIdleDelay = 1 seconds, - maxOnionMessagesPerSecond = 10 + maxOnionMessagesPerSecond = 10, + sendRemoteAddressInit = true, ), routerConf = RouterConf( + watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, - routerBroadcastInterval = 5 seconds, + routerBroadcastInterval = 1 day, // "disables" rebroadcast requestNodeAnnouncements = true, encodingType = EncodingType.COMPRESSED_ZLIB, channelRangeChunkSize = 20, @@ -181,34 +187,38 @@ object TestConstants { maxParts = 10, ), experimentName = "alice-test-experiment", - experimentPercentage = 100))) + experimentPercentage = 100))), + balanceEstimateHalfLife = 1 day, ), socksProxy_opt = None, maxPaymentAttempts = 5, enableTrampolinePayment = true, instanceId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), balanceCheckInterval = 1 hour, + blockchainWatchdogThreshold = 6, blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, - timeout = 1 minute - ) + timeout = 1 minute, + maxAttempts = 2, + ), + purgeInvoicesInterval = None, ) def channelParams: LocalParams = Peer.makeChannelParams( nodeParams, - nodeParams.features, + nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, - isFunder = true, + isInitiator = true, fundingSatoshis ).copy( - channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment + requestedChannelReserve_opt = Some(10_000 sat) // Bob will need to keep that much satoshis in his balance ) } object Bob { - val seed: ByteVector32 = ByteVector32(ByteVector.fill(32)(2)) + val seed: ByteVector32 = ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492") // 02bbbb... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) @@ -230,29 +240,40 @@ object TestConstants { PaymentMetadata -> Optional, ), pluginParams = Nil, - overrideFeatures = Map.empty, + overrideInitFeatures = Map.empty, syncWhitelist = Set.empty, - dustLimit = 1000 sat, - maxRemoteDustLimit = 1500 sat, + channelConf = ChannelConf( + dustLimit = 1000 sat, + maxRemoteDustLimit = 1500 sat, + maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs + maxAcceptedHtlcs = 30, + expiryDelta = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), + minFinalExpiryDelta = CltvExpiryDelta(18), + maxBlockProcessingDelay = 10 millis, + maxTxPublishRetryDelay = 10 millis, + htlcMinimum = 1000 msat, + minDepthBlocks = 3, + toRemoteDelay = CltvExpiryDelta(144), + maxToLocalDelay = CltvExpiryDelta(1000), + reserveToFundingRatio = 0.01, // note: not used (overridden below) + maxReserveToFundingRatio = 0.05, + unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + revocationTimeout = 20 seconds, + channelFlags = ChannelFlags.Public, + minFundingPublicSatoshis = 1000 sat, + minFundingPrivateSatoshis = 900 sat, + maxFundingSatoshis = 16777215 sat, + ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.75, 1.5, anchorOutputsFeeratePerKw, DustTolerance(30_000 sat, closeOnUpdateFeeOverflow = true)), perNodeFeerateTolerance = Map.empty ), - maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs - maxAcceptedHtlcs = 30, - expiryDelta = CltvExpiryDelta(144), - fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), - minFinalExpiryDelta = CltvExpiryDelta(18), - maxBlockProcessingDelay = 10 millis, - maxTxPublishRetryDelay = 10 millis, - htlcMinimum = 1000 msat, - minDepthBlocks = 3, - toRemoteDelay = CltvExpiryDelta(144), - maxToLocalDelay = CltvExpiryDelta(1000), relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, @@ -262,22 +283,15 @@ object TestConstants { feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionths = 30)), - reserveToFundingRatio = 0.01, // note: not used (overridden below) - maxReserveToFundingRatio = 0.05, - unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + feeProportionalMillionths = 30), + enforcementDelay = 10 minutes), db = TestDatabases.inMemoryDb(), - revocationTimeout = 20 seconds, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1, - watchSpentWindow = 1 second, - paymentRequestExpiry = 1 hour, + invoiceExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, - minFundingSatoshis = 1000 sat, - maxFundingSatoshis = 16777215 sat, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, initTimeout = 10 seconds, @@ -286,11 +300,13 @@ object TestConstants { pingDisconnect = true, maxRebroadcastDelay = 5 seconds, killIdleDelay = 10 seconds, - maxOnionMessagesPerSecond = 10 + maxOnionMessagesPerSecond = 10, + sendRemoteAddressInit = true, ), routerConf = RouterConf( + watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, - routerBroadcastInterval = 5 seconds, + routerBroadcastInterval = 1 day, // "disables" rebroadcast requestNodeAnnouncements = true, encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = 20, @@ -314,28 +330,33 @@ object TestConstants { maxParts = 10, ), experimentName = "bob-test-experiment", - experimentPercentage = 100))) + experimentPercentage = 100))), + balanceEstimateHalfLife = 1 day, ), socksProxy_opt = None, maxPaymentAttempts = 5, enableTrampolinePayment = true, instanceId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), balanceCheckInterval = 1 hour, + blockchainWatchdogThreshold = 6, blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, - timeout = 1 minute - ) + timeout = 1 minute, + maxAttempts = 2, + ), + purgeInvoicesInterval = None ) def channelParams: LocalParams = Peer.makeChannelParams( nodeParams, - nodeParams.features, + nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, - isFunder = false, - fundingSatoshis).copy( - channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment + isInitiator = false, + fundingSatoshis + ).copy( + requestedChannelReserve_opt = Some(20_000 sat) // Alice will need to keep that much satoshis in her balance ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 868707f4d3..91206d6721 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -3,8 +3,11 @@ package fr.acinq.eclair import akka.actor.ActorSystem import com.opentable.db.postgres.embedded.EmbeddedPostgres import com.zaxxer.hikari.HikariConfig +import fr.acinq.eclair.TestDatabases.TestPgDatabases.getNewDatabase import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.Databases.PostgresDatabases import fr.acinq.eclair.db._ +import fr.acinq.eclair.db.jdbc.JdbcUtils import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils.{PgLock, getVersion, using} import fr.acinq.eclair.db.sqlite.SqliteChannelsDb @@ -41,7 +44,7 @@ object TestDatabases { def inMemoryDb(): Databases = { val connection = sqliteInMemory() - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = None) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } @@ -56,21 +59,22 @@ object TestDatabases { * @param innerDb actual database instance */ class SqliteChannelsDbWithValidation(innerDb: SqliteChannelsDb) extends SqliteChannelsDb(innerDb.sqlite) { - override def addOrUpdateChannel(state: HasCommitments): Unit = { + override def addOrUpdateChannel(data: PersistentChannelData): Unit = { def freeze1(input: Origin): Origin = input match { case h: Origin.LocalHot => Origin.LocalCold(h.id) + case h: Origin.ChannelRelayedHot => Origin.ChannelRelayedCold(h.originChannelId, h.originHtlcId, h.amountIn, h.amountOut) case h: Origin.TrampolineRelayedHot => Origin.TrampolineRelayedCold(h.htlcs) - case _ => input + case c: Origin.Cold => c } def freeze2(input: Commitments): Commitments = input.copy(originChannels = input.originChannels.view.mapValues(o => freeze1(o)).toMap) // payment origins are always "cold" when deserialized, so to compare a "live" channel state against a state that has been // serialized and deserialized we need to turn "hot" payments into cold ones - def freeze3(input: HasCommitments): HasCommitments = input match { + def freeze3(input: PersistentChannelData): PersistentChannelData = input match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) @@ -78,9 +82,9 @@ object TestDatabases { case d: DATA_SHUTDOWN => d.copy(commitments = freeze2(d.commitments)) } - super.addOrUpdateChannel(state) - val check = super.getChannel(state.channelId) - val frozen = freeze3(state) + super.addOrUpdateChannel(data) + val check = super.getChannel(data.channelId) + val frozen = freeze3(data) require(check.contains(frozen), s"serialization/deserialization check failed, $check != $frozen") } } @@ -89,16 +93,18 @@ object TestDatabases { // @formatter:off override val connection: SQLiteConnection = sqliteInMemory() override lazy val db: Databases = { - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val jdbcUrlFile: File = new File(TestUtils.BUILD_DIRECTORY, s"jdbcUrlFile_${UUID.randomUUID()}.tmp") + jdbcUrlFile.deleteOnExit() + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = Some(jdbcUrlFile)) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } - override def close(): Unit = () + override def close(): Unit = connection.close() // @formatter:on } case class TestPgDatabases() extends TestDatabases { - private val pg = EmbeddedPostgres.start() - val datasource: DataSource = pg.getPostgresDatabase + + val datasource: DataSource = getNewDatabase() val hikariConfig = new HikariConfig hikariConfig.setDataSource(datasource) val lock: PgLock.LeaseLock = PgLock.LeaseLock(UUID.randomUUID(), 10 minutes, 8 minute, LockFailureHandler.logAndThrow, autoReleaseAtShutdown = false) @@ -109,13 +115,34 @@ object TestDatabases { implicit val system: ActorSystem = ActorSystem() // @formatter:off - override val connection: PgConnection = pg.getPostgresDatabase.getConnection.asInstanceOf[PgConnection] + override val connection: PgConnection = datasource.getConnection.asInstanceOf[PgConnection] // NB: we use a lazy val here: databases won't be initialized until we reference that variable override lazy val db: Databases = Databases.PostgresDatabases(hikariConfig, UUID.randomUUID(), lock, jdbcUrlFile_opt = Some(jdbcUrlFile), readOnlyUser_opt = None, resetJsonColumns = false, safetyChecks_opt = None) - override def close(): Unit = pg.close() + override def close(): Unit = { + db.asInstanceOf[PostgresDatabases].dataSource.close() + connection.close() + system.terminate() + } // @formatter:on } + object TestPgDatabases { + /** single instance */ + private val pg = EmbeddedPostgres.start() + + def getNewDatabase(): DataSource = { + implicit val datasource: DataSource = pg.getPostgresDatabase + val dbName = s"db_${randomBytes(8).toHex}" + JdbcUtils.withConnection { connection => + connection + .createStatement() + .executeUpdate(s"CREATE DATABASE $dbName") + } + pg.getDatabase("postgres", dbName) + } + + } + def forAllDbs(f: TestDatabases => Unit): Unit = { def using(dbs: TestDatabases)(g: TestDatabases => Unit): Unit = try g(dbs) finally dbs.close() // @formatter:off diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index ede577b057..c8012d440b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -16,17 +16,20 @@ package fr.acinq.eclair -import akka.actor.ActorRef -import akka.event.DiagnosticLoggingAdapter +import akka.actor.{ActorRef, ActorSystem} +import akka.event.{DiagnosticLoggingAdapter, EventStream} import akka.testkit.{TestActor, TestProbe} -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.Peer import fr.acinq.eclair.wire.protocol.LightningMessage +import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.concurrent.PatienceConfiguration import java.io.File import java.net.ServerSocket import java.nio.file.Files import java.util.UUID +import scala.concurrent.duration.{DurationInt, FiniteDuration} object TestUtils { @@ -36,7 +39,7 @@ object TestUtils { val BUILD_DIRECTORY = sys .props .get("buildDirectory") // this is defined if we run from maven - .getOrElse(new File(sys.props("user.dir"), "target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module + .getOrElse(new File(sys.props("user.dir"), "eclair-core/target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module def availablePort: Int = synchronized { var serverSocket: ServerSocket = null @@ -89,4 +92,22 @@ object TestUtils { seedFile } + /** + * Subscribing to [[EventStream]] is asynchronous, which can lead to race conditions. + * + * We use a dummy event subscription and poll until we receive a message, and rely on the fact that + * [[EventStream]] is an actor which means that all previous subscriptions have been taken into account. + */ + def waitEventStreamSynced(eventStream: EventStream)(implicit system: ActorSystem): Unit = { + val listener = TestProbe() + case class DummyEvent() + eventStream.subscribe(listener.ref, classOf[DummyEvent]) + eventually { + eventStream.publish(DummyEvent()) + assert(listener.msgAvailable) + } + } + + def waitFor(duration: FiniteDuration): Unit = Thread.sleep(duration.toMillis) + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala index 54535ed515..d07caab268 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala @@ -47,21 +47,21 @@ class UInt64Spec extends AnyFunSuite { assert(l1.toString == "9223372036854775808") assert(l1.toBigInt == BigInt("9223372036854775808")) - assert(a.toByteVector === hex"ffffffffffffffff") - assert(a.toString === "18446744073709551615") // 2^64 - 1 - assert(a.toBigInt === BigInt("18446744073709551615")) + assert(a.toByteVector == hex"ffffffffffffffff") + assert(a.toString == "18446744073709551615") // 2^64 - 1 + assert(a.toBigInt == BigInt("18446744073709551615")) - assert(b.toByteVector === hex"fffffffffffffffe") - assert(b.toString === "18446744073709551614") - assert(b.toBigInt === BigInt("18446744073709551614")) + assert(b.toByteVector == hex"fffffffffffffffe") + assert(b.toString == "18446744073709551614") + assert(b.toBigInt == BigInt("18446744073709551614")) - assert(c.toByteVector === hex"00000000000002a") - assert(c.toString === "42") - assert(c.toBigInt === BigInt("42")) + assert(c.toByteVector == hex"00000000000002a") + assert(c.toString == "42") + assert(c.toBigInt == BigInt("42")) - assert(z.toByteVector === hex"000000000000000") - assert(z.toString === "0") - assert(z.toBigInt === BigInt("0")) + assert(z.toByteVector == hex"000000000000000") + assert(z.toString == "0") + assert(z.toBigInt == BigInt("0")) assert(UInt64(hex"ff").toByteVector == hex"0000000000000ff") assert(UInt64(hex"800").toByteVector == hex"000000000000800") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index df616175f8..2c510a434c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.balance import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, MainAndHtlcBalance, OffChainBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{apply => _, _} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -12,7 +12,7 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL} import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.using -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.protocol.{CommitSig, Error, RevokeAndAck} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, randomBytes32} import org.scalatest.Outcome @@ -47,7 +47,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal === + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == MainAndHtlcBalance( toLocal = (TestConstants.fundingSatoshis - TestConstants.pushMsat - 30_000_000.msat).truncateToSatoshi, htlcs = 30_000.sat @@ -64,7 +64,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_SIGN() bob2alice.expectMsgType[CommitSig] - assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal === + assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == MainAndHtlcBalance( toLocal = TestConstants.pushMsat.truncateToSatoshi, htlcs = htlc.amountMsat.truncateToSatoshi @@ -97,7 +97,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -105,7 +105,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with )) // assuming alice gets the preimage for the 2nd htlc val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages1) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages1) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -146,7 +146,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) val Left(waitingForRevocation) = commitments.remoteNextCommitInfo - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -154,7 +154,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with )) // assuming alice gets the preimage for the 2nd htlc val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages1) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages1) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -179,14 +179,14 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, localCommitPublished), knownPreimages) === + assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, localCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), htlcs = Map.empty, @@ -207,15 +207,15 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 3rd-stage txs are published when htlc txs confirm val claimHtlcDelayedTxs = Seq(htlcTx1, htlcTx2, htlcTx3).map { htlcTimeoutTx => alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) claimHtlcDelayedTx } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) - assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), knownPreimages) === + assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcDelayedTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -230,7 +230,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sqlite = DriverManager.getConnection(s"jdbc:sqlite:$dbFile", sqliteConfig.toProperties) val channels = using(sqlite.createStatement) { statement => statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") - .mapCodec(stateDataCodec) + .mapCodec(channelDataCodec) } val knownPreimages: Set[(ByteVector32, Long)] = using(sqlite.prepareStatement("SELECT channel_id, htlc_id FROM pending_relay")) { statement => val rs = statement.executeQuery() @@ -245,7 +245,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with println(res.total) } - test("tx pruning") { _ => + test("tx pruning") { () => val txids = (for (_ <- 0 until 20) yield randomBytes32()).toList val knownTxids = Set(txids(1), txids(3), txids(4), txids(6), txids(9), txids(12), txids(13)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 74f4cbef3f..5476611fd6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits._ @@ -31,6 +32,7 @@ class DummyOnChainWallet extends OnChainWallet { import DummyOnChainWallet._ + val funded = collection.concurrent.TrieMap.empty[ByteVector32, Transaction] var rolledback = Set.empty[Transaction] override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) @@ -39,8 +41,11 @@ class DummyOnChainWallet extends OnChainWallet { override def getReceivePubkey(receiveAddress: Option[String] = None)(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = - Future.successful(DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount)) + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) + funded += (tx.fundingTx.txid -> tx.fundingTx) + Future.successful(tx) + } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) @@ -80,7 +85,7 @@ object DummyOnChainWallet { def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { val fundingTx = Transaction(version = 2, - txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) MakeFundingTxResponse(fundingTx, 0, 420 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala index e1ae26cb89..b0fdea68c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala @@ -16,8 +16,11 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{SigVersion, ScriptFlags} +import fr.acinq.bitcoin.SigHash.SIGHASH_ALL +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL /** * Created by PM on 27/01/2017. @@ -79,9 +82,9 @@ object WatcherSpec { */ def createUnspentTxChain(tx: Transaction, priv: PrivateKey): (Transaction, Transaction) = { // tx1 spends tx - val tx1 = createSpendP2WPKH(tx, priv, priv.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) + val tx1 = createSpendP2WPKH(tx, priv, priv.publicKey, 10000 sat, SEQUENCE_FINAL, 0) // and tx2 spends tx1 - val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) + val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, SEQUENCE_FINAL, 0) (tx1, tx2) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 0a07224cf8..e9a8e7c740 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -19,10 +19,11 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.SigVersion.SIGVERSION_WITNESS_V0 -import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} +import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword @@ -60,7 +61,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) val walletPassword = Random.alphanumeric.take(8).mkString sender.send(bitcoincli, BitcoinReq("encryptwallet", walletPassword)) - sender.expectMsgType[JString] + sender.expectMsgType[JString](60 seconds) restartBitcoind(sender) val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) @@ -84,12 +85,12 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(fundTxResponse.amountIn > 0.sat) assert(fundTxResponse.fee > 0.sat) fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.signatureScript.isEmpty && txIn.witness.isNull)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === TxIn.SEQUENCE_FINAL - 2)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence == bitcoin.TxIn.SEQUENCE_FINAL - 2)) bitcoinClient.signTransaction(fundTxResponse.tx, Nil).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] assert(signTxResponse.complete) - assert(signTxResponse.tx.txOut.size === 2) + assert(signTxResponse.tx.txOut.size == 2) bitcoinClient.publishTransaction(signTxResponse.tx).pipeTo(sender.ref) sender.expectMsg(signTxResponse.tx.txid) @@ -122,11 +123,11 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txManyOutputs = Transaction(2, Nil, TxOut(410000 sat, Script.pay2wpkh(randomKey().publicKey)) :: TxOut(230000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) bitcoinClient.fundTransaction(txManyOutputs, FundTransactionOptions(TestConstants.feeratePerKw, replaceable = false, changePosition = Some(1))).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] - assert(fundTxResponse.tx.txOut.size === 3) - assert(fundTxResponse.changePosition === Some(1)) + assert(fundTxResponse.tx.txOut.size == 3) + assert(fundTxResponse.changePosition == Some(1)) assert(!Set(230000 sat, 410000 sat).contains(fundTxResponse.tx.txOut(1).amount)) - assert(Set(230000 sat, 410000 sat) === Set(fundTxResponse.tx.txOut.head.amount, fundTxResponse.tx.txOut.last.amount)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === TxIn.SEQUENCE_FINAL - 1)) + assert(Set(230000 sat, 410000 sat) == Set(fundTxResponse.tx.txOut.head.amount, fundTxResponse.tx.txOut.last.amount)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence == bitcoin.TxIn.SEQUENCE_FINAL - 1)) } } @@ -148,11 +149,11 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(rpcClient) bitcoinClient.onChainBalance().pipeTo(sender.ref) - assert(sender.expectMsgType[OnChainBalance] === OnChainBalance(Satoshi(satoshi), Satoshi(satoshi))) + assert(sender.expectMsgType[OnChainBalance] == OnChainBalance(Satoshi(satoshi), Satoshi(satoshi))) bitcoinClient.fundTransaction(txIn, FundTransactionOptions(FeeratePerKw(250 sat))).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] - assert(fundTxResponse.fee === Satoshi(satoshi)) + assert(fundTxResponse.fee == Satoshi(satoshi)) } } @@ -181,7 +182,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A sender.expectMsgType[MakeFundingTxResponse].fundingTx } - assert(getLocks(sender).size === 4) + assert(getLocks(sender).size == 4) bitcoinClient.commit(fundingTxs(0)).pipeTo(sender.ref) assert(sender.expectMsgType[Boolean]) @@ -293,9 +294,9 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // tx2 replaced tx1 in the mempool bitcoinClient.getMempool().pipeTo(sender.ref) val mempoolTxs = sender.expectMsgType[Seq[Transaction]] - assert(mempoolTxs.length === 1) - assert(mempoolTxs.head.txid === tx2.txid) - assert(tx2.txIn.map(_.outPoint).intersect(tx1.txIn.map(_.outPoint)).length === 1) + assert(mempoolTxs.length == 1) + assert(mempoolTxs.head.txid == tx2.txid) + assert(tx2.txIn.map(_.outPoint).intersect(tx1.txIn.map(_.outPoint)).length == 1) } test("unlock transaction inputs if publishing fails") { @@ -407,13 +408,13 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A signTxResponse1.tx.txIn.tail.foreach(walletTxIn => assert(walletTxIn.witness.stack.nonEmpty)) // if the non-wallet inputs are signed, bitcoind signs the remaining wallet inputs. - val nonWalletSig = Transaction.signInput(txWithNonWalletInput, 0, Script.pay2pkh(nonWalletKey.publicKey), SIGHASH_ALL, txToRemote.txOut.head.amount, SIGVERSION_WITNESS_V0, nonWalletKey) + val nonWalletSig = Transaction.signInput(txWithNonWalletInput, 0, Script.pay2pkh(nonWalletKey.publicKey), bitcoin.SigHash.SIGHASH_ALL, txToRemote.txOut.head.amount, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, nonWalletKey) val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedNonWalletInput = txWithNonWalletInput.updateWitness(0, nonWalletWitness) bitcoinClient.signTransaction(txWithSignedNonWalletInput, Nil).pipeTo(sender.ref) val signTxResponse2 = sender.expectMsgType[SignTransactionResponse] assert(signTxResponse2.complete) - Transaction.correctlySpends(signTxResponse2.tx, txToRemote +: walletInputTxs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(signTxResponse2.tx, txToRemote +: walletInputTxs, bitcoin.ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // bitcoind does not sign inputs that have already been confirmed. @@ -441,7 +442,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(350000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] val txWithUnconfirmedInput = fundTxResponse.tx.copy(txIn = TxIn(OutPoint(unconfirmedTx, 0), ByteVector.empty, 0) +: fundTxResponse.tx.txIn) - val nonWalletSig = Transaction.signInput(txWithUnconfirmedInput, 0, Script.pay2pkh(nonWalletKey.publicKey), SIGHASH_ALL, unconfirmedTx.txOut.head.amount, SIGVERSION_WITNESS_V0, nonWalletKey) + val nonWalletSig = Transaction.signInput(txWithUnconfirmedInput, 0, Script.pay2pkh(nonWalletKey.publicKey), bitcoin.SigHash.SIGHASH_ALL, unconfirmedTx.txOut.head.amount, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, nonWalletKey) val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedUnconfirmedInput = txWithUnconfirmedInput.updateWitness(0, nonWalletWitness) val previousTx = PreviousTx(Transactions.InputInfo(OutPoint(unconfirmedTx.txid, 0), unconfirmedTx.txOut.head, Script.pay2pkh(nonWalletKey.publicKey)), nonWalletWitness) @@ -481,7 +482,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinrpcclient.invoke("createrawtransaction", Array(Map("txid" -> tx.txid.toHex, "vout" -> pos)), Map(address -> 5.999)).pipeTo(sender.ref) val JString(unsignedTxStr) = sender.expectMsgType[JValue] val unsignedTx = Transaction.read(unsignedTxStr) - val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(priv.publicKey), SIGHASH_ALL, 6.btc.toSatoshi, SIGVERSION_WITNESS_V0, priv) + val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(priv.publicKey), bitcoin.SigHash.SIGHASH_ALL, 6.btc.toSatoshi, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, priv) unsignedTx.updateWitness(0, Script.witnessPay2wpkh(priv.publicKey, sig)) } bitcoinClient.publishTransaction(spendingTx).pipeTo(sender.ref) @@ -539,7 +540,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.onChainBalance().pipeTo(sender.ref) val initialBalance = sender.expectMsgType[OnChainBalance] - assert(initialBalance.unconfirmed === 0.sat) + assert(initialBalance.unconfirmed == 0.sat) assert(initialBalance.confirmed > 50.btc.toSatoshi) val address = "n2YKngjUp139nkjKvZGnfLRN6HzzYxJsje" @@ -549,10 +550,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.listTransactions(25, 0).pipeTo(sender.ref) val Some(tx1) = sender.expectMsgType[List[WalletTx]].collectFirst { case tx if tx.txid == txid => tx } - assert(tx1.address === address) - assert(tx1.amount === -amount) + assert(tx1.address == address) + assert(tx1.amount == -amount) assert(tx1.fees < 0.sat) - assert(tx1.confirmations === 0) + assert(tx1.confirmations == 0) bitcoinClient.onChainBalance().pipeTo(sender.ref) // NB: we use + because these amounts are already negative @@ -561,10 +562,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A generateBlocks(1) bitcoinClient.listTransactions(25, 0).pipeTo(sender.ref) val Some(tx2) = sender.expectMsgType[List[WalletTx]].collectFirst { case tx if tx.txid == txid => tx } - assert(tx2.address === address) - assert(tx2.amount === -amount) + assert(tx2.address == address) + assert(tx2.amount == -amount) assert(tx2.fees < 0.sat) - assert(tx2.confirmations === 1) + assert(tx2.confirmations == 1) } test("get mempool transaction") { @@ -590,26 +591,26 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getMempoolTx(tx1.txid).pipeTo(sender.ref) val mempoolTx1 = sender.expectMsgType[MempoolTx] - assert(mempoolTx1.ancestorCount === 0) - assert(mempoolTx1.descendantCount === 2) - assert(mempoolTx1.fees === mempoolTx1.ancestorFees) - assert(mempoolTx1.descendantFees === mempoolTx1.fees + 12500.sat) + assert(mempoolTx1.ancestorCount == 0) + assert(mempoolTx1.descendantCount == 2) + assert(mempoolTx1.fees == mempoolTx1.ancestorFees) + assert(mempoolTx1.descendantFees == mempoolTx1.fees + 12500.sat) bitcoinClient.getMempoolTx(tx2.txid).pipeTo(sender.ref) val mempoolTx2 = sender.expectMsgType[MempoolTx] - assert(mempoolTx2.ancestorCount === 1) - assert(mempoolTx2.descendantCount === 1) - assert(mempoolTx2.fees === 5000.sat) - assert(mempoolTx2.descendantFees === 12500.sat) - assert(mempoolTx2.ancestorFees === mempoolTx1.fees + 5000.sat) + assert(mempoolTx2.ancestorCount == 1) + assert(mempoolTx2.descendantCount == 1) + assert(mempoolTx2.fees == 5000.sat) + assert(mempoolTx2.descendantFees == 12500.sat) + assert(mempoolTx2.ancestorFees == mempoolTx1.fees + 5000.sat) bitcoinClient.getMempoolTx(tx3.txid).pipeTo(sender.ref) val mempoolTx3 = sender.expectMsgType[MempoolTx] - assert(mempoolTx3.ancestorCount === 2) - assert(mempoolTx3.descendantCount === 0) - assert(mempoolTx3.fees === 7500.sat) - assert(mempoolTx3.descendantFees === mempoolTx3.fees) - assert(mempoolTx3.ancestorFees === mempoolTx1.fees + 12500.sat) + assert(mempoolTx3.ancestorCount == 2) + assert(mempoolTx3.descendantCount == 0) + assert(mempoolTx3.fees == 7500.sat) + assert(mempoolTx3.descendantFees == mempoolTx3.fees) + assert(mempoolTx3.ancestorFees == mempoolTx1.fees + 12500.sat) } test("abandon transaction") { @@ -655,25 +656,18 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) // first let's create a tx - val address = "n2YKngjUp139nkjKvZGnfLRN6HzzYxJsje" - bitcoinrpcclient.invoke("createrawtransaction", Array.empty, Map(address -> 6)).pipeTo(sender.ref) - val JString(noinputTx1) = sender.expectMsgType[JString] - bitcoinrpcclient.invoke("fundrawtransaction", noinputTx1).pipeTo(sender.ref) - val json = sender.expectMsgType[JValue] - val JString(unsignedtx1) = json \ "hex" - bitcoinrpcclient.invoke("signrawtransactionwithwallet", unsignedtx1).pipeTo(sender.ref) - val JString(signedTx1) = sender.expectMsgType[JValue] \ "hex" - val tx1 = Transaction.read(signedTx1) + val noInputTx1 = Transaction(2, Nil, Seq(TxOut(500_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) + bitcoinClient.fundTransaction(noInputTx1, FundTransactionOptions(FeeratePerKw(2500 sat))).pipeTo(sender.ref) + val unsignedTx1 = sender.expectMsgType[FundTransactionResponse].tx + bitcoinClient.signTransaction(unsignedTx1).pipeTo(sender.ref) + val tx1 = sender.expectMsgType[SignTransactionResponse].tx // let's then generate another tx that double spends the first one - val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray - bitcoinrpcclient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount).sum.toLong * 1.0 / 1e8)).pipeTo(sender.ref) - val JString(unsignedtx2) = sender.expectMsgType[JValue] - bitcoinrpcclient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) - val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" - val tx2 = Transaction.read(signedTx2) - - // tx1/tx2 haven't been published, so tx1 isn't double spent + val unsignedTx2 = tx1.copy(txOut = Seq(TxOut(tx1.txOut.map(_.amount).sum, Script.pay2wpkh(randomKey().publicKey)))) + bitcoinClient.signTransaction(unsignedTx2).pipeTo(sender.ref) + val tx2 = sender.expectMsgType[SignTransactionResponse].tx + + // tx1/tx2 haven't been published, so tx1 isn't double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(false) // let's publish tx2 @@ -682,11 +676,79 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // tx2 hasn't been confirmed so tx1 is still not considered double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(false) + // tx2 isn't considered double-spent either + bitcoinClient.doubleSpent(tx2).pipeTo(sender.ref) + sender.expectMsg(false) // let's confirm tx2 generateBlocks(1) - // this time tx1 has been double spent + // this time tx1 has been double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(true) + // and tx2 isn't considered double-spent since it's confirmed + bitcoinClient.doubleSpent(tx2).pipeTo(sender.ref) + sender.expectMsg(false) + } + + test("detect if tx has been double-spent (with unconfirmed inputs)") { + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + val priv = randomKey() + + // Let's create one confirmed and one unconfirmed utxo. + val (confirmedParentTx, unconfirmedParentTx) = { + val txs = Seq(400_000 sat, 500_000 sat).map(amount => { + val noInputTx = Transaction(2, Nil, Seq(TxOut(amount, Script.pay2wpkh(priv.publicKey))), 0) + bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(FeeratePerKw(2500 sat), lockUtxos = true)).pipeTo(sender.ref) + val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx + bitcoinClient.signTransaction(unsignedTx).pipeTo(sender.ref) + sender.expectMsgType[SignTransactionResponse].tx + }) + bitcoinClient.publishTransaction(txs.head).pipeTo(sender.ref) + sender.expectMsg(txs.head.txid) + generateBlocks(1) + bitcoinClient.publishTransaction(txs.last).pipeTo(sender.ref) + sender.expectMsg(txs.last.txid) + (txs.head, txs.last) + } + + // Let's spend those unconfirmed utxos. + val childTx = createSpendManyP2WPKH(Seq(confirmedParentTx, unconfirmedParentTx), priv, priv.publicKey, 500 sat, 0, 0) + // The tx hasn't been published, so it isn't double-spent. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + // We publish the tx and verify it isn't double-spent. + bitcoinClient.publishTransaction(childTx).pipeTo(sender.ref) + sender.expectMsg(childTx.txid) + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + + // We double-spend the unconfirmed parent, which evicts our child transaction. + { + val previousAmountOut = unconfirmedParentTx.txOut.map(_.amount).sum + val unsignedTx = unconfirmedParentTx.copy(txOut = Seq(TxOut(previousAmountOut - 50_000.sat, Script.pay2wpkh(randomKey().publicKey)))) + bitcoinClient.signTransaction(unsignedTx).pipeTo(sender.ref) + val signedTx = sender.expectMsgType[SignTransactionResponse].tx + bitcoinClient.publishTransaction(signedTx).pipeTo(sender.ref) + sender.expectMsg(signedTx.txid) + } + + // We can't know whether the child transaction is double-spent or not, as its unconfirmed input is now unknown: it's + // not in the blockchain nor in the mempool. This unknown input may reappear in the future and the tx could then be + // published again. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + + // We double-spend the confirmed input. + val spendingTx = createSpendP2WPKH(confirmedParentTx, priv, priv.publicKey, 600 sat, 0, 0) + bitcoinClient.publishTransaction(spendingTx).pipeTo(sender.ref) + sender.expectMsg(spendingTx.txid) + // While the spending transaction is unconfirmed, we don't consider our transaction double-spent. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + // Once the spending transaction confirms, we know that our transaction is double-spent. + generateBlocks(1) + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(true) } test("find spending transaction of a given output") { @@ -719,7 +781,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A generateBlocks(1) bitcoinClient.getBlockHeight().pipeTo(sender.ref) val blockHeight1 = sender.expectMsgType[BlockHeight] - assert(blockHeight1 === blockHeight + 1) + assert(blockHeight1 == blockHeight + 1) bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(1)) bitcoinClient.isTransactionOutputSpendable(tx1.txid, 0, includeMempool = false).pipeTo(sender.ref) @@ -741,7 +803,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getReceivePubkey(receiveAddress = Some(address)).pipeTo(sender.ref) val receiveKey = sender.expectMsgType[PublicKey] - assert(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash) === Script.pay2wpkh(receiveKey)) + assert(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash) == Script.pay2wpkh(receiveKey)) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 5018a79097..d51266e49c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -19,15 +19,15 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, Btc, BtcAmount, ByteVector32, MilliBtc, OutPoint, Satoshi, Transaction, computeP2WpkhAddress} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcAmount, ByteVector32, MilliBtc, OutPoint, Satoshi, Transaction, computeP2WpkhAddress} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.{SafeCookie, UserPassword} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCAuthMethod, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import fr.acinq.eclair.{BlockHeight, TestUtils, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ +import sttp.client3.okhttp.OkHttpFutureBackend import java.io.File import java.nio.file.Files @@ -66,7 +66,7 @@ trait BitcoindService extends Logging { var bitcoinrpcauthmethod: BitcoinJsonRPCAuthMethod = _ var bitcoincli: ActorRef = _ - def startBitcoind(useCookie: Boolean = false): Unit = { + def startBitcoind(useCookie: Boolean = false, startupFlags: String = ""): Unit = { Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) { val is = classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf") @@ -87,7 +87,7 @@ trait BitcoindService extends Logging { Files.writeString(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, conf) } - bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() + bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR $startupFlags".run() bitcoinrpcauthmethod = if (useCookie) { SafeCookie(s"$PATH_BITCOIND_DATADIR/regtest/.cookie") } else { @@ -111,12 +111,14 @@ trait BitcoindService extends Logging { bitcoind.exitValue() } - def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false): Unit = { + def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false, startupFlags: String = "", loadWallet: Boolean = true): Unit = { stopBitcoind() - startBitcoind(useCookie = useCookie) + startBitcoind(useCookie = useCookie, startupFlags = startupFlags) waitForBitcoindUp(sender) - sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet)) - sender.expectMsgType[JValue] + if (loadWallet) { + sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet)) + sender.expectMsgType[JValue] + } } private def waitForBitcoindUp(sender: TestProbe): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 892301371e..544b9481db 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOp import akka.actor.{ActorRef, Props, typed} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.blockchain.WatcherSpec._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -29,7 +29,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{FundTransactio import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NewTransaction} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -95,7 +95,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind restartBitcoind(probe) generateBlocks(1) val block2 = listener.expectMsgType[CurrentBlockHeight] - assert(block2.blockHeight === block1.blockHeight + 1) + assert(block2.blockHeight == block1.blockHeight + 1) listener.expectNoMessage(100 millis) }) } @@ -108,8 +108,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val w1 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) val w2 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) - val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, ShortChannelId(1)) - val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, ShortChannelId(1)) + val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, RealShortChannelId(1)) + val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, RealShortChannelId(1)) val w5 = WatchFundingConfirmed(TestProbe().ref, txid, 3) // we test as if the collection was immutable @@ -141,17 +141,17 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // When the watcher starts, it broadcasts the current height. val block1 = listener.expectMsgType[CurrentBlockHeight] - assert(blockHeight.get() === block1.blockHeight.toLong) + assert(blockHeight.get() == block1.blockHeight.toLong) listener.expectNoMessage(100 millis) generateBlocks(1) - assert(listener.expectMsgType[CurrentBlockHeight].blockHeight === block1.blockHeight + 1) - assert(blockHeight.get() === block1.blockHeight.toLong + 1) + assert(listener.expectMsgType[CurrentBlockHeight].blockHeight == block1.blockHeight + 1) + assert(blockHeight.get() == block1.blockHeight.toLong + 1) listener.expectNoMessage(100 millis) generateBlocks(5) - assert(listener.expectMsgType[CurrentBlockHeight].blockHeight === block1.blockHeight + 6) - assert(blockHeight.get() === block1.blockHeight.toLong + 6) + assert(listener.expectMsgType[CurrentBlockHeight].blockHeight == block1.blockHeight + 6) + assert(blockHeight.get() == block1.blockHeight.toLong + 6) listener.expectNoMessage(100 millis) }) } @@ -170,17 +170,17 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 2) + assert(listener.expectMsgType[Set[Watch[_]]].size == 2) generateBlocks(1) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 1) + assert(listener.expectMsgType[Set[Watch[_]]].size == 1) generateBlocks(3) - assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) @@ -188,11 +188,11 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // If we try to watch a transaction that has already been confirmed, we should immediately receive a WatchEventConfirmed. watcher ! WatchFundingConfirmed(listener.ref, tx.txid, 1) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) watcher ! WatchFundingConfirmed(listener.ref, tx.txid, 2) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) watcher ! WatchFundingDeeplyBuried(listener.ref, tx.txid, 4) - assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) @@ -210,18 +210,18 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val (tx1, tx2) = createUnspentTxChain(tx, priv) val listener = TestProbe() - watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, ShortChannelId(5)) + watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, RealShortChannelId(5)) watcher ! WatchFundingSpent(listener.ref, tx.txid, outputIndex, Set.empty) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 2) + assert(listener.expectMsgType[Set[Watch[_]]].size == 2) bitcoinClient.publishTransaction(tx1).pipeTo(probe.ref) probe.expectMsg(tx1.txid) // tx and tx1 aren't confirmed yet, but we trigger the WatchEventSpent when we see tx1 in the mempool. listener.expectMsgAllOf( - WatchExternalChannelSpentTriggered(ShortChannelId(5)), + WatchExternalChannelSpentTriggered(RealShortChannelId(5)), WatchFundingSpentTriggered(tx1) ) // Let's confirm tx and tx1: seeing tx1 in a block should trigger WatchEventSpent again, but not WatchEventSpentBasic @@ -234,7 +234,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! ListWatches(listener.ref) val watches1 = listener.expectMsgType[Set[Watch[_]]] - assert(watches1.size === 1) + assert(watches1.size == 1) assert(watches1.forall(_.isInstanceOf[WatchFundingSpent])) // Let's submit tx2, and set a watch after it has been confirmed this time. @@ -248,7 +248,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! ListWatches(listener.ref) val watches2 = listener.expectMsgType[Set[Watch[_]]] - assert(watches2.size === 1) + assert(watches2.size == 1) assert(watches2.forall(_.isInstanceOf[WatchFundingSpent])) watcher ! StopWatching(listener.ref) @@ -267,8 +267,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectMsg(WatchOutputSpentTriggered(tx1)) watcher ! StopWatching(listener.ref) - watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, ShortChannelId(1)) - listener.expectMsg(WatchExternalChannelSpentTriggered(ShortChannelId(1))) + watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, RealShortChannelId(1)) + listener.expectMsg(WatchExternalChannelSpentTriggered(RealShortChannelId(1))) watcher ! WatchFundingSpent(listener.ref, tx1.txid, 0, Set.empty) listener.expectMsg(WatchFundingSpentTriggered(tx2)) }) @@ -302,7 +302,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind generateBlocks(1) probe.expectMsg(WatchFundingSpentTriggered(tx2)) // tx2 is confirmed which triggers WatchEventSpent again generateBlocks(1) - assert(probe.expectMsgType[WatchFundingConfirmedTriggered].tx === tx1) // tx1 now has 3 confirmations + assert(probe.expectMsgType[WatchFundingConfirmedTriggered].tx == tx1) // tx1 now has 3 confirmations }) } @@ -350,7 +350,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchOutputSpent(actor1.ref, txid, 1, Set.empty) watcher ! ListWatches(actor1.ref) val watches1 = actor1.expectMsgType[Set[Watch[_]]] - assert(watches1.size === 6) + assert(watches1.size == 6) watcher ! WatchFundingConfirmed(actor2.ref, txid, 2) watcher ! WatchFundingDeeplyBuried(actor2.ref, txid, 3) @@ -359,7 +359,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchOutputSpent(actor2.ref, txid, 1, Set.empty) watcher ! ListWatches(actor2.ref) val watches2 = actor2.expectMsgType[Set[Watch[_]]] - assert(watches2.size === 11) + assert(watches2.size == 11) assert(watches1.forall(w => watches2.contains(w))) watcher ! StopWatching(actor2.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 71142a332d..090519e9b4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.fee import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestKitBaseClass import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient @@ -117,7 +117,7 @@ class BitcoinCoreFeeProviderSpec extends TestKitBaseClass with BitcoindService w regtestProvider.mempoolMinFee().pipeTo(sender.ref) val mempoolMinFee = sender.expectMsgType[FeeratePerKB] // The regtest provider doesn't have any transaction in its mempool, so it defaults to the min_relay_fee. - assert(mempoolMinFee.feerate.toLong === FeeratePerKw.MinimumRelayFeeRate) + assert(mempoolMinFee.feerate.toLong == FeeratePerKw.MinimumRelayFeeRate) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala deleted file mode 100644 index 4315b44f59..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import akka.util.Timeout -import fr.acinq.bitcoin.SatoshiLong -import fr.acinq.eclair.TestDatabases -import fr.acinq.eclair.db.sqlite.SqliteFeeratesDb -import org.scalatest.funsuite.AnyFunSuite - -import scala.concurrent.Await -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ - -class DbFeeProviderSpec extends AnyFunSuite { - - val feerates1: FeeratesPerKB = FeeratesPerKB(FeeratePerKB(800 sat), FeeratePerKB(100 sat), FeeratePerKB(200 sat), FeeratePerKB(300 sat), FeeratePerKB(400 sat), FeeratePerKB(500 sat), FeeratePerKB(600 sat), FeeratePerKB(700 sat), FeeratePerKB(800 sat)) - - test("db fee provider saves feerates in database") { - val sqlite = TestDatabases.sqliteInMemory() - val db = new SqliteFeeratesDb(sqlite) - val provider = new DbFeeProvider(db, new ConstantFeeProvider(feerates1)) - - assert(db.getFeerates().isEmpty) - assert(Await.result(provider.getFeerates, Timeout(30 seconds).duration) == feerates1) - assert(db.getFeerates().get == feerates1) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index 00cc29c7df..0112b2ecf9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.duration._ @@ -55,19 +55,19 @@ class FallbackFeeProviderSpec extends AnyFunSuite { val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil, FeeratePerByte(1 sat)) - assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider1.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider7.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider7.feeratesPerKB) } test("ensure minimum feerate") { @@ -75,7 +75,7 @@ class FallbackFeeProviderSpec extends AnyFunSuite { val minFeeratePerByte = FeeratePerByte(2 sat) val minFeeratePerKB = FeeratePerKB(minFeeratePerByte) val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, minFeeratePerByte) - assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(FeeratePerKB(64000 sat), FeeratePerKB(32000 sat), FeeratePerKB(16000 sat), FeeratePerKB(8000 sat), FeeratePerKB(4000 sat), minFeeratePerKB, minFeeratePerKB, minFeeratePerKB, minFeeratePerKB)) + assert(await(fallbackFeeProvider.getFeerates) == FeeratesPerKB(FeeratePerKB(64000 sat), FeeratePerKB(32000 sat), FeeratePerKB(16000 sat), FeeratePerKB(8000 sat), FeeratePerKB(4000 sat), minFeeratePerKB, minFeeratePerKB, minFeeratePerKB, minFeeratePerKB)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 138ad1c8e5..7543f199ba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.CurrentFeerates import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.{TestFeeEstimator, randomKey} @@ -27,7 +27,7 @@ class FeeEstimatorSpec extends AnyFunSuite { val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { - val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat))) @@ -38,13 +38,13 @@ class FeeEstimatorSpec extends AnyFunSuite { test("get commitment feerate") { val feeEstimator = new TestFeeEstimator() val channelType = ChannelTypes.Standard - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat))) - assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) === FeeratePerKw(5000 sat)) + assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) == FeeratePerKw(5000 sat)) val currentFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(4000 sat))) - assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat)) + assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, Some(currentFeerates)) == FeeratePerKw(4000 sat)) } test("get commitment feerate (anchor outputs)") { @@ -53,38 +53,38 @@ class FeeEstimatorSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5, mempoolMinFee = FeeratePerKw(250 sat))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) val highFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) } test("fee difference too high") { @@ -102,7 +102,7 @@ class FeeEstimatorSpec extends AnyFunSuite { (FeeratePerKw(250 sat), FeeratePerKw(1500 sat), true), ) testCases.foreach { case (networkFeerate, proposedFeerate, expected) => - assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected) + assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) == expected) } } @@ -123,7 +123,7 @@ class FeeEstimatorSpec extends AnyFunSuite { ) testCases.foreach { case (networkFeerate, proposedFeerate) => assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate)) - assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, networkFeerate, proposedFeerate)) + assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), networkFeerate, proposedFeerate)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala index be9c0e16a8..73c4d4548f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala @@ -16,24 +16,24 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.fee.FeeratePerKw.MinimumFeeratePerKw import org.scalatest.funsuite.AnyFunSuite class FeeProviderSpec extends AnyFunSuite { test("convert fee rates") { - assert(FeeratePerByte(FeeratePerKw(2000 sat)) === FeeratePerByte(8 sat)) - assert(FeeratePerKB(FeeratePerByte(10 sat)) === FeeratePerKB(10000 sat)) - assert(FeeratePerKB(FeeratePerKw(25 sat)) === FeeratePerKB(100 sat)) - assert(FeeratePerKw(FeeratePerKB(10000 sat)) === FeeratePerKw(2500 sat)) - assert(FeeratePerKw(FeeratePerByte(10 sat)) === FeeratePerKw(2500 sat)) + assert(FeeratePerByte(FeeratePerKw(2000 sat)) == FeeratePerByte(8 sat)) + assert(FeeratePerKB(FeeratePerByte(10 sat)) == FeeratePerKB(10000 sat)) + assert(FeeratePerKB(FeeratePerKw(25 sat)) == FeeratePerKB(100 sat)) + assert(FeeratePerKw(FeeratePerKB(10000 sat)) == FeeratePerKw(2500 sat)) + assert(FeeratePerKw(FeeratePerByte(10 sat)) == FeeratePerKw(2500 sat)) } test("enforce a minimum feerate-per-kw") { - assert(FeeratePerKw(FeeratePerKB(1000 sat)) === MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerKB(500 sat)) === MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerByte(1 sat)) === MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerKB(1000 sat)) == MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerKB(500 sat)) == MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerByte(1 sat)) == MinimumFeeratePerKw) } test("compare feerates") { @@ -48,12 +48,12 @@ class FeeProviderSpec extends AnyFunSuite { } test("numeric operations") { - assert(FeeratePerKw(100 sat) * 0.4 === FeeratePerKw(40 sat)) - assert(FeeratePerKw(501 sat) * 0.5 === FeeratePerKw(250 sat)) - assert(FeeratePerKw(5 sat) * 5 === FeeratePerKw(25 sat)) - assert(FeeratePerKw(100 sat) / 10 === FeeratePerKw(10 sat)) - assert(FeeratePerKw(101 sat) / 10 === FeeratePerKw(10 sat)) - assert(FeeratePerKw(100 sat) + FeeratePerKw(50 sat) === FeeratePerKw(150 sat)) + assert(FeeratePerKw(100 sat) * 0.4 == FeeratePerKw(40 sat)) + assert(FeeratePerKw(501 sat) * 0.5 == FeeratePerKw(250 sat)) + assert(FeeratePerKw(5 sat) * 5 == FeeratePerKw(25 sat)) + assert(FeeratePerKw(100 sat) / 10 == FeeratePerKw(10 sat)) + assert(FeeratePerKw(101 sat) / 10 == FeeratePerKw(10 sat)) + assert(FeeratePerKw(100 sat) + FeeratePerKw(50 sat) == FeeratePerKw(150 sat)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 0267b5b747..05160c8c98 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.ExecutionContext.Implicits.global diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala index 68f71c0efb..819579ce96 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.eventstream.EventStream import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{DangerousBlocksSkew, WrappedCurrentBlockHeight} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, NodeParams, TestConstants, TestTags} @@ -53,8 +53,8 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - // assert(events.map(_.recentHeaders.source).toSet === Set("bitcoinheaders.net", "blockcypher.com", "blockstream.info", "mempool.space")) - assert(events.map(_.recentHeaders.source).toSet === Set("bitcoinheaders.net", "blockstream.info", "mempool.space")) + // assert(events.map(_.recentHeaders.source).toSet == Set("bitcoinheaders.net", "blockcypher.com", "blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("bitcoinheaders.net", "blockstream.info", "mempool.space")) testKit.stop(watchdog) } @@ -70,8 +70,8 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - // assert(events.map(_.recentHeaders.source).toSet === Set("blockcypher.com", "blockstream.info", "mempool.space")) - assert(events.map(_.recentHeaders.source).toSet === Set("blockstream.info", "mempool.space")) + // assert(events.map(_.recentHeaders.source).toSet == Set("blockcypher.com", "blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("blockstream.info", "mempool.space")) testKit.stop(watchdog) } @@ -82,21 +82,21 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa val watchdog = testKit.spawn(BlockchainWatchdog(nodeParamsTestnet, 1 second, blockTimeout)) watchdog ! WrappedCurrentBlockHeight(BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) // If we don't receive blocks, we check blockchain sources. - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) // And we keep checking blockchain sources until we receive a block. - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) } @@ -123,7 +123,7 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - assert(events.map(_.recentHeaders.source).toSet === Set("blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("blockstream.info", "mempool.space")) testKit.stop(watchdog) } else { cancel("Tor daemon is not up and running") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala index 0d127ea1a9..846292dc1b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.LatestHeaders import fr.acinq.eclair.blockchain.watchdogs.ExplorerApi.{BlockcypherExplorer, BlockstreamExplorer, CheckLatestHeaders, MempoolSpaceExplorer} import fr.acinq.eclair.{BlockHeight, TestTags} @@ -28,7 +28,7 @@ class ExplorerApiSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl implicit val sttpBackend = ExplorerApi.createSttpBackend(None) - val explorers = Seq(BlockcypherExplorer(None), BlockstreamExplorer(None), MempoolSpaceExplorer(None)) + val explorers = Seq(BlockcypherExplorer(), BlockstreamExplorer(useTorEndpoints = false), MempoolSpaceExplorer(useTorEndpoints = false)) test("fetch latest block headers", TestTags.ExternalApi) { for (explorer <- explorers) { @@ -36,10 +36,10 @@ class ExplorerApiSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val sender = testKit.createTestProbe[LatestHeaders]() api ! CheckLatestHeaders(sender.ref) val latestHeaders = sender.expectMessageType[LatestHeaders] - assert(latestHeaders.currentBlockHeight === BlockHeight(630450)) + assert(latestHeaders.currentBlockHeight == BlockHeight(630450)) assert(latestHeaders.blockHeaders.nonEmpty) assert(latestHeaders.blockHeaders.forall(_.blockHeight > BlockHeight(630450))) - assert(latestHeaders.source === explorer.name) + assert(latestHeaders.source == explorer.name) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala index 410ae07bc5..27cd7f45df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala @@ -18,7 +18,8 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, BlockHeader} +import fr.acinq.bitcoin.scalacompat.Block +import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders} import fr.acinq.eclair.blockchain.watchdogs.HeadersOverDns.CheckLatestHeaders import fr.acinq.eclair.{BlockHeight, TestTags} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 48983db99f..e86490470a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -17,17 +17,18 @@ package fr.acinq.eclair.channel import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc} -import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass} +import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} +import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector -class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -69,7 +70,11 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel Fixture(alice, HtlcWithPreimage(rb2, htlcb2), bob, HtlcWithPreimage(ra2, htlca2), TestProbe()) } - test("local commit published") { + case class LocalFixture(nodeParams: NodeParams, alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, htlcTimeoutTxs: Seq[HtlcTimeoutTx], htlcSuccessTxs: Seq[HtlcSuccessTx], probe: TestProbe) { + val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] + } + + private def setupClosingChannelForLocalClose(): LocalFixture = { val f = setupClosingChannel() import f._ @@ -77,15 +82,15 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] assert(aliceClosing.localCommitPublished.nonEmpty) val lcp = aliceClosing.localCommitPublished.get - assert(lcp.commitTx.txOut.length === 6) + assert(lcp.commitTx.txOut.length == 6) assert(lcp.claimMainDelayedOutputTx.nonEmpty) - assert(lcp.htlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(lcp.htlcTxs.size == 4) // we have one entry for each non-dust htlc val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length === 2) + assert(htlcTimeoutTxs.length == 2) val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(htlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = lcp.htlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(lcp.claimHtlcDelayedTxs.length === 0) // we will publish 3rd-stage txs once htlc txs confirm + assert(lcp.claimHtlcDelayedTxs.length == 0) // we will publish 3rd-stage txs once htlc txs confirm assert(!lcp.isConfirmed) assert(!lcp.isDone) @@ -104,128 +109,171 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(bobClosing.remoteCommitPublished.nonEmpty) val rcp = bobClosing.remoteCommitPublished.get - // Scenario 1: our HTLC txs are confirmed, they claim the remaining HTLC - { - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + LocalFixture(nodeParams, f.alice, alicePendingHtlc, remainingHtlcOutpoint, lcp2, rcp, htlcTimeoutTxs, htlcSuccessTxs, probe) + } - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + test("local commit published (our HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForLocalClose() + import f._ - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - assert(theirClaimHtlcTimeout !== None) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) - assert(lcp5.isDone) + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length == 3) - // Scenario 2: our HTLC txs are confirmed and we claim the remaining HTLC - { - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp4.isDone) - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) + assert(theirClaimHtlcTimeout !== None) + val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) + assert(lcp5.isDone) + } + + test("local commit published (our HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForLocalClose() + import f._ + + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) + } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length == 3) + + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp4.isDone) + + alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] + val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) + assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) + assert(lcp5.claimHtlcDelayedTxs.length == 3) - alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) - assert(lcp5.claimHtlcDelayedTxs.length === 3) + val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx + val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + assert(lcp6.claimHtlcDelayedTxs.length == 4) - val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx - val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp6.claimHtlcDelayedTxs.length === 4) + val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) + assert(!lcp7.isDone) - val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) - assert(!lcp7.isDone) + val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) + assert(lcp8.isDone) + } + + test("local commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForLocalClose() + import f._ - val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) - assert(lcp8.isDone) + val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, _) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) } + assert(lcp3.claimHtlcDelayedTxs.length == 1) + assert(!lcp3.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, _) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(lcp3.claimHtlcDelayedTxs.length === 1) - assert(!lcp3.isDone) + val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) + assert(!lcp4.isDone) + + val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingHtlcTimeoutTxs.length == 1) + val (lcp5, Some(remainingClaimHtlcTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + assert(lcp5.claimHtlcDelayedTxs.length == 2) - val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) - assert(!lcp4.isDone) + val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp6.isDone) - val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingHtlcTimeoutTxs.length === 1) - val (lcp5, Some(remainingClaimHtlcTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp5.claimHtlcDelayedTxs.length === 2) + val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) + val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) + assert(lcp7.isDone) + } - val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp6.isDone) + test("local commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForLocalClose() + import f._ - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) - assert(lcp7.isDone) + val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length == 2) - // Scenario 4: they get back the HTLCs they sent us - { - val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 2) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp4.isDone) - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) + assert(remoteHtlcTimeoutTxs.length == 2) + val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) + assert(!lcp5.isDone) + + val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) + assert(lcp6.isDone) + } + + test("local commit published (our HTLC txs are confirmed and the remaining HTLC is failed)") { + val f = setupClosingChannelForLocalClose() + import f._ - val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) - assert(remoteHtlcTimeoutTxs.length === 2) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) - assert(!lcp5.isDone) + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) + } + + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length == 3) - val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) - assert(lcp6.isDone) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) } + assert(!lcp4.isDone) + + // at this point the pending incoming htlc is waiting for a preimage + assert(lcp4.htlcTxs(remainingHtlcOutpoint) == None) + + alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer), replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] + val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] + val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) + assert(!lcp5.htlcTxs.contains(remainingHtlcOutpoint)) + assert(lcp5.claimHtlcDelayedTxs.length == 3) + + assert(lcp5.isDone) } - test("remote commit published") { + case class RemoteFixture(bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, claimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], claimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], probe: TestProbe) + + private def setupClosingChannelForRemoteClose(): RemoteFixture = { val f = setupClosingChannel() import f._ val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] assert(bobClosing.remoteCommitPublished.nonEmpty) val rcp = bobClosing.remoteCommitPublished.get - assert(rcp.commitTx.txOut.length === 6) + assert(rcp.commitTx.txOut.length == 6) assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 2) + assert(claimHtlcTimeoutTxs.length == 2) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head assert(!rcp.isConfirmed) assert(!rcp.isDone) @@ -245,73 +293,104 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(aliceClosing.localCommitPublished.nonEmpty) val lcp = aliceClosing.localCommitPublished.get - // Scenario 1: our claim-HTLC txs are confirmed, they claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + RemoteFixture(f.bob, f.bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) + } - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) + test("remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForRemoteClose() + import f._ + + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 2: our claim-HTLC txs are confirmed and we claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) + assert(rcp4.isDone) + } - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + test("remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) + val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) + assert(rcp5.isDone) + } + + test("remote commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForRemoteClose() + import f._ + + val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get + val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) + } + assert(!rcp3.isDone) + + val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingClaimHtlcTimeoutTx.length == 1) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) + assert(!rcp4.isDone) + + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) + assert(rcp5.isDone) + } - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) + test("remote commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) + val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 4: they get back the HTLCs they sent us - { - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) + assert(!rcp4.isDone) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) + assert(rcp5.isDone) + } + + test("remote commit published (our claim-HTLC txs are confirmed and the remaining one is failed)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) + + bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, Right(UnknownNextPeer), replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(!rcp4.claimHtlcTxs.contains(remainingHtlcOutpoint)) + assert(rcp4.claimHtlcTxs.size == 3) + assert(getClaimHtlcSuccessTxs(rcp4).size == 1) + assert(getClaimHtlcTimeoutTxs(rcp4).size == 2) + + assert(rcp4.isDone) } - test("next remote commit published") { + private def setupClosingChannelForNextRemoteClose(): RemoteFixture = { val probe = TestProbe() val setup = init() reachNormal(setup) @@ -352,13 +431,13 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] assert(bobClosing.nextRemoteCommitPublished.nonEmpty) val rcp = bobClosing.nextRemoteCommitPublished.get - assert(rcp.commitTx.txOut.length === 6) + assert(rcp.commitTx.txOut.length == 6) assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 2) + assert(claimHtlcTimeoutTxs.length == 2) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head assert(!rcp.isConfirmed) assert(!rcp.isDone) @@ -374,70 +453,82 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(rcp2.isConfirmed) assert(!rcp2.isDone) - // Scenario 1: our claim-HTLC txs are confirmed, they claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val bobPendingHtlc = HtlcWithPreimage(ra2, htlca2) + + RemoteFixture(bob, bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) + } + + test("next remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 2: our claim-HTLC txs are confirmed and we claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) + assert(rcp4.isDone) + } - bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + test("next remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) + val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) + assert(rcp5.isDone) + } - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) + test("next remote commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) + val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get + val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 4: they get back the HTLCs they sent us - { - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingClaimHtlcTimeoutTx.length == 1) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) + assert(!rcp4.isDone) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) + assert(rcp5.isDone) + } + + test("next remote commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) + val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) + + val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) + assert(!rcp4.isDone) + + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) + assert(rcp5.isDone) } test("revoked commit published") { @@ -461,11 +552,11 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel alice ! WatchFundingSpentTriggered(revokedCommitTx) awaitCond(alice.stateName == CLOSING) val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.revokedCommitPublished.length === 1) + assert(aliceClosing.revokedCommitPublished.length == 1) val rvk = aliceClosing.revokedCommitPublished.head assert(rvk.claimMainOutputTx.nonEmpty) assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.length === 4) + assert(rvk.htlcPenaltyTxs.length == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) assert(!rvk.isDone) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index e4f169cd28..cf82814d26 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -18,12 +18,12 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{Features, TestKitBaseClass} +import fr.acinq.eclair.{Feature, Features, InitFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike -class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { test("channel features determines commitment format") { val standardChannel = ChannelFeatures() @@ -33,64 +33,76 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) assert(!standardChannel.hasFeature(Features.AnchorOutputs)) - assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(standardChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) assert(!standardChannel.paysDirectlyToWallet) assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey)) assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs)) - assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(staticRemoteKeyChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) assert(staticRemoteKeyChannel.paysDirectlyToWallet) assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) - assert(anchorOutputsChannel.commitmentFormat === Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(anchorOutputsChannel.commitmentFormat == Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) assert(!anchorOutputsChannel.paysDirectlyToWallet) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) - assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat == Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) } test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType) + case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expectedChannelType: ChannelType) val testCases = Seq( - TestCase(Features.empty, Features.empty, ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features.empty, Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType) + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, announceChannel = testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") } } test("create channel type from features") { + case class TestCase(features: Features[InitFeature], expectedChannelType: ChannelType) + val validChannelTypes = Seq( - Features.empty -> ChannelTypes.Standard, - Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, - Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, + TestCase(Features.empty, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) - for ((features, expected) <- validChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === expected) + for (testCase <- validChannelTypes) { + assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType, testCase.features) } - val invalidChannelTypes = Seq( + val invalidChannelTypes: Seq[Features[InitFeature]] = Seq( Features(Wumbo -> Optional), Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional), @@ -105,20 +117,29 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) + assert(ChannelTypes.fromFeatures(features) == ChannelTypes.UnsupportedChannelType(features), features) } } - test("enrich channel type with other permanent channel features") { - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).features.isEmpty) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).features === Set(StaticRemoteKey)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) + test("enrich channel type with optional permanent channel features") { + case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expected: Set[Feature]) + val testCases = Seq( + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty, announceChannel = true, Set.empty), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional), announceChannel = true, Set(Wumbo)), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional), announceChannel = true, Set(Wumbo)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty, announceChannel = true, Set(StaticRemoteKey)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, Wumbo)), + TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs)), + TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features.empty, Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), Features.empty, Features(Wumbo -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), Features.empty, Features(Wumbo -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), + ) + testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures, t.announceChannel).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 0ac2406757..50b09aad80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -16,13 +16,14 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Commitments._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.crypto.keymanager.LocalChannelKeyManager import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} @@ -43,6 +44,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val feeConfNoMismatch = OnChainFeeConf( FeeTargets(6, 2, 12, 2, 6, 1), new TestFeeEstimator(), + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = false, 1.0, FeerateTolerance(0.00001, 100000.0, TestConstants.anchorOutputsFeeratePerKw, DustTolerance(100000 sat, closeOnUpdateFeeOverflow = false)), @@ -380,11 +382,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // See https://github.com/lightningnetwork/lightning-rfc/issues/728 test("funder keeps additional reserve to avoid channel being stuck") { f => - val isFunder = true - val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isFunder) + val isInitiator = true + val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) val Right((c1, _)) = sendAdd(c, cmdAdd, f.currentBlockHeight, feeConfNoMismatch) - assert(c1.availableBalanceForSend === 0.msat) + assert(c1.availableBalanceForSend == 0.msat) // We should be able to handle a fee increase. val Right((c2, _)) = sendFee(c1, CMD_UPDATE_FEE(FeeratePerKw(3000 sat)), feeConfNoMismatch) @@ -395,8 +397,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("can send availableForSend") { f => - for (isFunder <- Seq(true, false)) { - val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isFunder) + for (isInitiator <- Seq(true, false)) { + val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) val result = sendAdd(c, cmdAdd, f.currentBlockHeight, feeConfNoMismatch) assert(result.isRight, result) @@ -404,8 +406,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("can receive availableForReceive") { f => - for (isFunder <- Seq(true, false)) { - val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, FeeratePerKw(2679 sat), 546 sat, isFunder) + for (isInitiator <- Seq(true, false)) { + val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) val add = UpdateAddHtlc(randomBytes32(), c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket) receiveAdd(c, add, feeConfNoMismatch) } @@ -413,17 +415,17 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("should always be able to send availableForSend", Tag("fuzzy")) { f => val maxPendingHtlcAmount = 1000000.msat - case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) + case class FuzzTest(isInitiator: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) for (_ <- 1 to 100) { val t = FuzzTest( - isFunder = Random.nextInt(2) == 0, + isInitiator = Random.nextInt(2) == 0, pendingHtlcs = Random.nextInt(10), feeRatePerKw = FeeratePerKw(Random.nextInt(10000).max(1).sat), dustLimit = Random.nextInt(1000).sat, // We make sure both sides have enough to send/receive at least the initial pending HTLCs. toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat, toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat) - var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder) + var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isInitiator) // Add some initial HTLCs to the pending list (bigger commit tx). for (_ <- 0 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) @@ -441,17 +443,17 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("should always be able to receive availableForReceive", Tag("fuzzy")) { f => val maxPendingHtlcAmount = 1000000.msat - case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) + case class FuzzTest(isInitiator: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) for (_ <- 1 to 100) { val t = FuzzTest( - isFunder = Random.nextInt(2) == 0, + isInitiator = Random.nextInt(2) == 0, pendingHtlcs = Random.nextInt(10), feeRatePerKw = FeeratePerKw(Random.nextInt(10000).max(1).sat), dustLimit = Random.nextInt(1000).sat, // We make sure both sides have enough to send/receive at least the initial pending HTLCs. toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat, toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat) - var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder) + var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isInitiator) // Add some initial HTLCs to the pending list (bigger commit tx). for (_ <- 0 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) @@ -469,13 +471,18 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } + test("check if channel seed has been modified") { f => + val commitments = f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments + assert(Commitments.validateSeed(commitments, TestConstants.Alice.channelKeyManager)) + assert(!Commitments.validateSeed(commitments, new LocalChannelKeyManager(ByteVector32.fromValidHex("42" * 32), Block.RegtestGenesisBlock.hash))) + } } object CommitmentsSpec { - def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = { - val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, Features.empty) - val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isInitiator: Boolean = true, announceChannel: Boolean = true): Commitments = { + val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), @@ -483,7 +490,7 @@ object CommitmentsSpec { ChannelFeatures(), localParams, remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = announceChannel), LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil), RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomBytes32(), randomKey().publicKey), LocalChanges(Nil, Nil, Nil), @@ -497,8 +504,8 @@ object CommitmentsSpec { } def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = { - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, Features.empty) - val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), @@ -506,7 +513,7 @@ object CommitmentsSpec { ChannelFeatures(), localParams, remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = announceChannel), LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil), RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomBytes32(), randomKey().publicKey), LocalChanges(Nil, Nil, Nil), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala index 6dbff89b48..c277653adc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc @@ -42,18 +42,18 @@ class DustExposureSpec extends AnyFunSuiteLike { OutgoingHtlc(createHtlc(3, 500.sat.toMilliSatoshi)), ) val spec = CommitmentSpec(htlcs, FeeratePerKw(FeeratePerByte(50 sat)), 50000 msat, 75000 msat) - assert(DustExposure.computeExposure(spec, 450 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === 898.sat.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, 500 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === 2796.sat.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, 500 sat, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 3796.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 450 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 898.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 500 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 2796.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 500 sat, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 3796.sat.toMilliSatoshi) } { // Low feerate: buffer adds 10 sat/byte val dustLimit = 500.sat val feerate = FeeratePerKw(FeeratePerByte(10 sat)) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) === 2257.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) === 2157.sat) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) === 4015.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) === 3815.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2257.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2157.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 4015.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 3815.sat) val htlcs = Set[DirectedHtlc]( // Below the dust limit. IncomingHtlc(createHtlc(0, 450.sat.toMilliSatoshi)), @@ -70,8 +70,8 @@ class DustExposureSpec extends AnyFunSuiteLike { ) val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) val expected = 450.sat + 450.sat + 2250.sat + 2150.sat + 4010.sat + 3810.sat - assert(DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat) === expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 2, dustLimit, Transactions.DefaultCommitmentFormat) === DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat) == expected.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, feerate * 2, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 4010.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 3810.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 4020.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) @@ -81,10 +81,10 @@ class DustExposureSpec extends AnyFunSuiteLike { // High feerate: buffer adds 25% val dustLimit = 1000.sat val feerate = FeeratePerKw(FeeratePerByte(80 sat)) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 15120.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 14320.sat) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 18650.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 17650.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 15120.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 14320.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 18650.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 17650.sat) val htlcs = Set[DirectedHtlc]( // Below the dust limit. IncomingHtlc(createHtlc(0, 900.sat.toMilliSatoshi)), @@ -101,8 +101,8 @@ class DustExposureSpec extends AnyFunSuiteLike { ) val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) val expected = 900.sat + 900.sat + 15000.sat + 14000.sat + 18000.sat + 17000.sat - assert(DustExposure.computeExposure(spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 1.25, dustLimit, Transactions.DefaultCommitmentFormat) === DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.computeExposure(spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == expected.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, feerate * 1.25, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 18000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 17000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 19000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) @@ -113,7 +113,7 @@ class DustExposureSpec extends AnyFunSuiteLike { test("filter incoming htlcs before forwarding") { val dustLimit = 1000.sat val initialSpec = CommitmentSpec(Set.empty, FeeratePerKw(10000 sat), 0 msat, 0 msat) - assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.DefaultCommitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 0.msat) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) // NB: HTLC-success transactions are bigger than HTLC-timeout transactions: that means incoming htlcs have a higher @@ -128,7 +128,7 @@ class DustExposureSpec extends AnyFunSuiteLike { OutgoingHtlc(createHtlc(3, 9500.sat.toMilliSatoshi)), IncomingHtlc(createHtlc(4, 9500.sat.toMilliSatoshi)), )) - assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.DefaultCommitmentFormat) === 18500.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 18500.sat.toMilliSatoshi) val receivedHtlcs = Seq( createHtlc(5, 9500.sat.toMilliSatoshi), @@ -139,8 +139,8 @@ class DustExposureSpec extends AnyFunSuiteLike { createHtlc(10, 50000.sat.toMilliSatoshi), ) val (accepted, rejected) = DustExposure.filterBeforeForward(25000 sat, updatedSpec, dustLimit, 10000.sat.toMilliSatoshi, initialSpec, dustLimit, 15000.sat.toMilliSatoshi, receivedHtlcs, Transactions.DefaultCommitmentFormat) - assert(accepted.map(_.id).toSet === Set(5, 6, 8, 10)) - assert(rejected.map(_.id).toSet === Set(7, 9)) + assert(accepted.map(_.id).toSet == Set(5, 6, 8, 10)) + assert(rejected.map(_.id).toSet == Set(7, 9)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 28fd3dae94..9d3af27cb5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -19,15 +19,16 @@ package fr.acinq.eclair.channel import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment @@ -51,7 +52,7 @@ import scala.util.Random class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase with Logging { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], pipe: ActorRef, relayerA: ActorRef, relayerB: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], pipe: ActorRef, alice2relayer: ActorRef, bob2relayer: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) override def withFixture(test: OneArgTest): Outcome = { val fuzzy = test.tags.contains("fuzzy") @@ -64,22 +65,22 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe TestUtils.forwardOutgoingToPipe(bobPeer, pipe) val alice2blockchain = TestProbe() val bob2blockchain = TestProbe() - val registerA = system.actorOf(Props(new TestRegister())) - val registerB = system.actorOf(Props(new TestRegister())) - val paymentHandlerA = system.actorOf(Props(new PaymentHandler(aliceParams, registerA))) - val paymentHandlerB = system.actorOf(Props(new PaymentHandler(bobParams, registerB))) - val relayerA = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, registerA, paymentHandlerA)) - val relayerB = system.actorOf(Relayer.props(bobParams, TestProbe().ref, registerB, paymentHandlerB)) + val aliceRegister = system.actorOf(Props(new TestRegister())) + val bobRegister = system.actorOf(Props(new TestRegister())) + val alicePaymentHandler = system.actorOf(Props(new PaymentHandler(aliceParams, aliceRegister))) + val bobPaymentHandler = system.actorOf(Props(new PaymentHandler(bobParams, bobRegister))) + val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler)) + val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler)) val wallet = new DummyOnChainWallet() - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, relayerA, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, relayerB, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, aliceRelayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, bobRelayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) within(30 seconds) { val aliceInit = Init(Alice.channelParams.initFeatures) val bobInit = Init(Bob.channelParams.initFeatures) - registerA ! alice - registerB ! bob + aliceRegister ! alice + bobRegister ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelTypes.Standard) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -99,7 +100,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe awaitCond(alice.stateName == NORMAL, 1 minute) awaitCond(bob.stateName == NORMAL, 1 minute) } - withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, aliceRelayer, bobRelayer, alicePaymentHandler, bobPaymentHandler))) } class TestRegister() extends Actor with ActorLogging { @@ -122,14 +123,14 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1 + OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, Some(paymentSecret), None)).get._1 } def initiatePaymentOrStop(remaining: Int): Unit = if (remaining > 0) { paymentHandler ! ReceivePayment(Some(requiredAmount), Left("One coffee")) context become { - case req: PaymentRequest => + case req: Invoice => sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId, req.paymentSecret.get) context become { case RES_SUCCESS(_: CMD_ADD_HTLC, _) => () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 185b2f4e7a..d521d6c5f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -18,15 +18,16 @@ package fr.acinq.eclair.channel import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestConstants.Alice.nodeParams import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} +import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -34,18 +35,19 @@ import scodec.bits.HexStringSyntax import java.util.UUID import scala.concurrent.duration._ -class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging test("compute the funding tx min depth according to funding amount") { - assert(Helpers.minDepthForFunding(nodeParams, Btc(1)) == 4) - assert(Helpers.minDepthForFunding(nodeParams.copy(minDepthBlocks = 6), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 - assert(Helpers.minDepthForFunding(nodeParams, Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC - assert(Helpers.minDepthForFunding(nodeParams, Btc(12.50)) == 31) - assert(Helpers.minDepthForFunding(nodeParams, Btc(12.60)) == 32) - assert(Helpers.minDepthForFunding(nodeParams, Btc(30)) == 73) - assert(Helpers.minDepthForFunding(nodeParams, Btc(50)) == 121) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(1)).contains(4)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), ChannelFeatures(), Btc(1)).contains(6)) // 4 conf would be enough but we use min-depth=6 + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(6.25)).contains(16)) // we use scaling_factor=15 and a fixed block reward of 6.25BTC + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.50)).contains(31)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.60)).contains(32)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(30)).contains(73)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(50)).contains(121)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(Features.ZeroConf), Btc(50)).isEmpty) } test("compute refresh delay") { @@ -97,18 +99,18 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat awaitCond(bob.stateName == CLOSING) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size === 6) + assert(lcp.htlcTxs.size == 6) val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length === 3) + assert(htlcTimeoutTxs.length == 3) val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length === 1) + assert(htlcSuccessTxs.length == 1) val rcp = bob.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 6) + assert(rcp.claimHtlcTxs.size == 6) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 3) + assert(claimHtlcTimeoutTxs.length == 3) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) + assert(claimHtlcSuccessTxs.length == 1) Fixture(alice, lcp, Set(htlca1a, htlca1b, htlca2), bob, rcp, Set(htlcb1a, htlcb1b, htlcb2), probe) } @@ -176,7 +178,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat def findTimedOutHtlcs(f: Fixture, withoutHtlcId: Boolean): Unit = { import f._ - val dustLimit = alice.underlyingActor.nodeParams.dustLimit + val dustLimit = alice.underlyingActor.nodeParams.channelConf.dustLimit val commitmentFormat = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.localCommit val remoteCommit = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.remoteCommit @@ -196,24 +198,24 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(aliceTimedOutHtlcs.toSet === aliceHtlcs) + assert(aliceTimedOutHtlcs.toSet == aliceHtlcs) val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(bobTimedOutHtlcs.toSet === bobHtlcs) + assert(bobTimedOutHtlcs.toSet == bobHtlcs) val bobTimedOutHtlcs2 = claimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(bobTimedOutHtlcs2.toSet === bobHtlcs) + assert(bobTimedOutHtlcs2.toSet == bobHtlcs) htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) @@ -259,12 +261,12 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat ClosingTx(InputInfo(OutPoint(ByteVector32.Zeroes, 0), TxOut(1000 sat, Nil), Nil), Transaction(2, Nil, txOut, 0), None) } - assert(Closing.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2pkhBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2shBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2wshBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(futureSegwitBelowDust))) + assert(Closing.MutualClose.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2pkhBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2shBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2wshBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(futureSegwitBelowDust))) } test("tell closing type") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala index b32ec0d9bd..ec4ab0cea6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala @@ -4,8 +4,8 @@ import fr.acinq.eclair._ import akka.actor.{ActorRef, Props} import akka.testkit.TestProbe -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.ParallelTestExecution @@ -15,10 +15,10 @@ class RegisterSpec extends TestKitBaseClass with AnyFunSuiteLike with ParallelTe test("register processes custom restored events") { val sender = TestProbe() - val registerRef = system.actorOf(Props(new Register)) + val registerRef = system.actorOf(Register.props()) val customRestoredEvent = CustomChannelRestored(TestProbe().ref, randomBytes32(), TestProbe().ref, randomKey().publicKey) registerRef ! customRestoredEvent sender.send(registerRef, Symbol("channels")) - sender.expectMsgType[Map[ByteVector32, ActorRef]] === Map(customRestoredEvent.channelId -> customRestoredEvent.channel) + sender.expectMsgType[Map[ByteVector32, ActorRef]] == Map(customRestoredEvent.channelId -> customRestoredEvent.channel) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 577ea871cd..256d4b90df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -5,18 +5,21 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit import akka.testkit.{TestActor, TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, CommitSig, Error, FundingLocked, Init, RevokeAndAck} +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck} import fr.acinq.eclair.{TestKitBaseClass, _} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -28,26 +31,23 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan type FixtureParam = SetupFixture override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val setup = init() within(30 seconds) { reachNormal(setup) withFixture(test.toNoArgTest(setup)) } } - def aliceInit = Init(Alice.nodeParams.features) + private def aliceInit = Init(Alice.nodeParams.features.initFeatures()) - def bobInit = Init(Bob.nodeParams.features) + private def bobInit = Init(Bob.nodeParams.features.initFeatures()) test("use funding pubkeys from publish commitment to spend our output") { f => import f._ val sender = TestProbe() // we start by storing the current state - val oldStateData = alice.stateData.asInstanceOf[HasCommitments] + val oldStateData = alice.stateData.asInstanceOf[PersistentChannelData] // then we add an htlc and sign it addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN()) @@ -67,7 +67,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan alice.stop() // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) // then we reconnect them @@ -82,7 +82,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan bob2alice.forward(newAlice) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(newAlice)).getMessage) + assert(new String(error.data.toArray) == PleasePublishYourCommitment(channelId(newAlice)).getMessage) // alice now waits for bob to publish its commitment awaitCond(newAlice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) @@ -116,7 +116,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan // spend our output val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = bitcoin.TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey().publicKey)) :: Nil, lockTime = 0) @@ -133,14 +133,14 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan /** We are only interested in channel updates from Alice, we use the channel flag to discriminate */ def aliceChannelUpdateListener(channelUpdateListener: TestProbe): TestProbe = { val aliceListener = TestProbe() - channelUpdateListener.setAutoPilot(new testkit.TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { + channelUpdateListener.setAutoPilot { + (sender: ActorRef, msg: Any) => msg match { case u: ChannelUpdateParametersChanged if u.channelUpdate.channelFlags.isNode1 == Announcements.isNode1(Alice.nodeParams.nodeId, Bob.nodeParams.nodeId) => aliceListener.ref.tell(msg, sender) TestActor.KeepRunning case _ => TestActor.KeepRunning } - }) + } aliceListener } @@ -167,7 +167,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan alice.stop() // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) @@ -203,6 +203,10 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan // and we terminate Alice alice.stop() + // there should ne no pending messages + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + // we restart Alice with different configurations Seq( Alice.nodeParams @@ -210,20 +214,20 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan Alice.nodeParams .modify(_.relayParams.privateChannelFees.feeProportionalMillionths).setTo(2345), Alice.nodeParams - .modify(_.expiryDelta).setTo(CltvExpiryDelta(147)), + .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), Alice.nodeParams .modify(_.relayParams.privateChannelFees.feeProportionalMillionths).setTo(2345) - .modify(_.expiryDelta).setTo(CltvExpiryDelta(147)), + .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), ) foreach { newConfig => - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) val u1 = channelUpdateListener.expectMsgType[ChannelUpdateParametersChanged] assert(!Announcements.areSameIgnoreFlags(u1.channelUpdate, oldStateData.channelUpdate)) - assert(u1.channelUpdate.feeBaseMsat === newConfig.relayParams.privateChannelFees.feeBase) - assert(u1.channelUpdate.feeProportionalMillionths === newConfig.relayParams.privateChannelFees.feeProportionalMillionths) - assert(u1.channelUpdate.cltvExpiryDelta === newConfig.expiryDelta) + assert(u1.channelUpdate.feeBaseMsat == newConfig.relayParams.privateChannelFees.feeBase) + assert(u1.channelUpdate.feeProportionalMillionths == newConfig.relayParams.privateChannelFees.feeProportionalMillionths) + assert(u1.channelUpdate.cltvExpiryDelta == newConfig.channelConf.expiryDelta) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) @@ -231,10 +235,15 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan bob2alice.expectMsgType[ChannelReestablish] alice2bob.forward(bob) bob2alice.forward(newAlice) - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] alice2bob.forward(bob) bob2alice.forward(newAlice) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + awaitCond(newAlice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala index f007ac7d5f..fb4df104f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.WatcherSpec.createSpendP2WPKH import fr.acinq.eclair.blockchain.bitcoind.BitcoindService @@ -83,8 +83,8 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi publisher ! Publish(probe.ref, cmd) val w = watcher.expectMsgType[WatchParentTxConfirmed] - assert(w.txId === parentTx.txid) - assert(w.minDepth === 5) + assert(w.txId == parentTx.txid) + assert(w.minDepth == 5) createBlocks(5, probe) w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, parentTx) @@ -143,8 +143,8 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi createBlocks(1, probe) val response = probe.expectMsgType[TxRejected] - assert(response.cmd === cmd) - assert(response.reason === ConflictingTxConfirmed) + assert(response.cmd == cmd) + assert(response.reason == ConflictingTxConfirmed) // The actor should stop when requested: probe.watch(publisher.toClassic) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 705f29aa78..e197f5bc92 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.ActorRef -import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService @@ -70,6 +70,20 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi awaitCond(getMempool(bitcoinClient, probe).exists(_.txid == txId)) } + test("transaction still in mempool with unconfirmed parent") { + val f = createFixture() + import f._ + + val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 1_000 sat, 0, 0) + monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 50 sat) + waitTxInMempool(bitcoinClient, tx.txid, probe) + + // NB: we don't really generate a block, we're testing the case where both txs are still in the mempool. + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) + probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(), parentConfirmed = false)) + probe.expectNoMessage(100 millis) + } + test("transaction confirmed") { val f = createFixture() import f._ @@ -82,18 +96,18 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi waitTxInMempool(bitcoinClient, tx.txid, probe) // NB: we don't really generate a block, we're testing the case where the tx is still in the mempool. - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) + probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(), parentConfirmed = true)) probe.expectNoMessage(100 millis) - assert(TestConstants.Alice.nodeParams.minDepthBlocks > 1) + assert(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks > 1) generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRecentlyConfirmed(tx.txid, 1)) probe.expectNoMessage(100 millis) // we wait for more than one confirmation to protect against reorgs - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks - 1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks - 1) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxDeeplyBuried(tx)) } @@ -109,8 +123,8 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi monitor ! Publish(probe.ref, tx2, tx2.txIn.head.outPoint, "test-tx", 10 sat) waitTxInMempool(bitcoinClient, tx2.txid, probe) - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxDeeplyBuried(tx2)) } @@ -148,7 +162,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0) val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0))) monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone)) + probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone)) } test("publish failed (confirmed parent, wallet input doesn't exist)") { @@ -161,7 +175,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0) val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0))) monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone)) + probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone)) } test("publish failed (wallet input spent by conflicting confirmed transaction)") { @@ -176,7 +190,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendManyP2WPKH(Seq(parentTx, walletTx), priv, priv.publicKey, 5_000 sat, 0, 0) monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(tx.txid, WalletInputGone)) + probe.expectMsg(TxRejected(tx.txid, InputGone)) } test("publish succeeds then transaction is replaced by an unconfirmed tx") { @@ -192,7 +206,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi probe.expectMsg(tx2.txid) // When a new block is found, we detect that the transaction has been replaced. - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRejected(tx1.txid, ConflictingTxUnconfirmed)) } @@ -210,7 +224,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // When a new block is found, we detect that the transaction has been replaced. generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRejected(tx1.txid, ConflictingTxConfirmed)) } @@ -234,8 +248,8 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // When a new block is found, we detect that the transaction has been evicted. generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - probe.expectMsg(TxRejected(tx.txid, WalletInputGone)) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) + probe.expectMsg(TxRejected(tx.txid, InputGone)) } test("emit transaction events") { @@ -253,12 +267,12 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 15 sat) waitTxInMempool(bitcoinClient, tx.txid, probe) val txPublished = eventListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === tx) - assert(txPublished.miningFee === 15.sat) - assert(txPublished.desc === "test-tx") + assert(txPublished.tx == tx) + assert(txPublished.miningFee == 15.sat) + assert(txPublished.desc == "test-tx") - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) eventListener.expectMsg(TransactionConfirmed(txPublished.channelId, txPublished.remoteNodeId, tx)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala index d44f10dd28..16aa7226a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel.publish -import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.AdjustPreviousTxOutputResult.{AddWalletInputs, TxOutputAdjusted} @@ -68,8 +68,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { txOut = TxOut(amountOut, Script.pay2wpkh(PlaceHolderPubKey)) :: Nil, ))) val adjustedTx = adjustAnchorOutputChange(unsignedTx, commitTx.tx, amountIn, commitFeerate, targetFeerate, dustLimit) - assert(adjustedTx.txInfo.tx.txIn.size === unsignedTx.txInfo.tx.txIn.size) - assert(adjustedTx.txInfo.tx.txOut.size === 1) + assert(adjustedTx.txInfo.tx.txIn.size == unsignedTx.txInfo.tx.txIn.size) + assert(adjustedTx.txInfo.tx.txOut.size == 1) assert(adjustedTx.txInfo.tx.txOut.head.amount >= dustLimit) if (adjustedTx.txInfo.tx.txOut.head.amount > dustLimit) { // Simulate tx signing to check final feerate. @@ -133,8 +133,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { for (unsignedTx <- Seq(unsignedHtlcSuccessTx, unsignedHtlcTimeoutTx)) { val totalAmountIn = unsignedTx.txInfo.input.txOut.amount + walletAmountIn val adjustedTx = adjustHtlcTxChange(unsignedTx, totalAmountIn, targetFeerate, dustLimit, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(adjustedTx.txInfo.tx.txIn.size === unsignedTx.txInfo.tx.txIn.size) - assert(adjustedTx.txInfo.tx.txOut.size === 1 || adjustedTx.txInfo.tx.txOut.size === 2) + assert(adjustedTx.txInfo.tx.txIn.size == unsignedTx.txInfo.tx.txIn.size) + assert(adjustedTx.txInfo.tx.txOut.size == 1 || adjustedTx.txInfo.tx.txOut.size == 2) if (adjustedTx.txInfo.tx.txOut.size == 2) { // Simulate tx signing to check final feerate. val signedTx = { @@ -184,8 +184,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { adjustClaimHtlcTxOutput(claimHtlc, targetFeerate, dustLimit) match { case Left(_) => assert(targetFeerate >= FeeratePerKw(7000 sat)) case Right(updatedClaimHtlc) => - assert(updatedClaimHtlc.txInfo.tx.txIn.length === 1) - assert(updatedClaimHtlc.txInfo.tx.txOut.length === 1) + assert(updatedClaimHtlc.txInfo.tx.txIn.length == 1) + assert(updatedClaimHtlc.txInfo.tx.txOut.length == 1) assert(updatedClaimHtlc.txInfo.tx.txOut.head.amount < previousAmount) previousAmount = updatedClaimHtlc.txInfo.tx.txOut.head.amount val signedTx = updatedClaimHtlc match { @@ -223,16 +223,16 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { // We can handle a small feerate update by lowering the change output. val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(feerateUpdate1.txInfo.tx.txIn === previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length === 1) + assert(feerateUpdate1.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) + assert(feerateUpdate1.txInfo.tx.txOut.length == 1) val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitments) - assert(feerateUpdate2.txInfo.tx.txIn === previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length === 1) + assert(feerateUpdate2.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) + assert(feerateUpdate2.txInfo.tx.txOut.length == 1) assert(feerateUpdate2.txInfo.tx.txOut.head.amount < feerateUpdate1.txInfo.tx.txOut.head.amount) // But if the feerate increase is too large, we must add new wallet inputs. val AddWalletInputs(previousTx) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(10000 sat), commitments) - assert(previousTx === previousAnchorTx) + assert(previousTx == previousAnchorTx) } test("adjust previous htlc transaction outputs", Tag("fuzzy")) { @@ -259,19 +259,19 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { // We can handle a small feerate update by lowering the change output. val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(feerateUpdate1.txInfo.tx.txIn === previousTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length === 2) - assert(feerateUpdate1.txInfo.tx.txOut.head === previousTx.txInfo.tx.txOut.head) + assert(feerateUpdate1.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) + assert(feerateUpdate1.txInfo.tx.txOut.length == 2) + assert(feerateUpdate1.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitments) - assert(feerateUpdate2.txInfo.tx.txIn === previousTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length === 2) - assert(feerateUpdate2.txInfo.tx.txOut.head === previousTx.txInfo.tx.txOut.head) + assert(feerateUpdate2.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) + assert(feerateUpdate2.txInfo.tx.txOut.length == 2) + assert(feerateUpdate2.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) assert(feerateUpdate2.txInfo.tx.txOut.last.amount < feerateUpdate1.txInfo.tx.txOut.last.amount) // If the previous funding attempt didn't add a change output, we must add new wallet inputs. val previousTxNoChange = previousTx.updateTx(previousTx.txInfo.tx.copy(txOut = Seq(previousTx.txInfo.tx.txOut.head))) val AddWalletInputs(tx) = adjustPreviousTxOutput(FundedTx(previousTxNoChange, 25000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(tx === previousTxNoChange) + assert(tx == previousTxNoChange) for (_ <- 1 to 100) { val amountIn = Random.nextInt(25_000_000).sat @@ -283,11 +283,11 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { val targetFeerate = FeeratePerKw(2500 sat) + FeeratePerKw(Random.nextInt(20000).sat) adjustPreviousTxOutput(FundedTx(fuzzyPreviousTx, amountIn, FeeratePerKw(2500 sat)), targetFeerate, commitments) match { case AdjustPreviousTxOutputResult.Skip(_) => // nothing do check - case AddWalletInputs(tx) => assert(tx === fuzzyPreviousTx) + case AddWalletInputs(tx) => assert(tx == fuzzyPreviousTx) case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn === fuzzyPreviousTx.txInfo.tx.txIn) + assert(updatedTx.txInfo.tx.txIn == fuzzyPreviousTx.txInfo.tx.txIn) assert(Set(1, 2).contains(updatedTx.txInfo.tx.txOut.length)) - assert(updatedTx.txInfo.tx.txOut.head === fuzzyPreviousTx.txInfo.tx.txOut.head) + assert(updatedTx.txInfo.tx.txOut.head == fuzzyPreviousTx.txInfo.tx.txOut.head) assert(updatedTx.txInfo.tx.txOut.last.amount >= 600.sat) } } @@ -308,8 +308,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { case AdjustPreviousTxOutputResult.Skip(_) => assert(targetFeerate >= FeeratePerKw(10000 sat)) case AddWalletInputs(_) => fail("shouldn't add wallet inputs to claim-htlc-tx") case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn === claimHtlc.txInfo.tx.txIn) - assert(updatedTx.txInfo.tx.txOut.length === 1) + assert(updatedTx.txInfo.tx.txIn == claimHtlc.txInfo.tx.txIn) + assert(updatedTx.txInfo.tx.txOut.length == 1) assert(updatedTx.txInfo.tx.txOut.head.amount < previousAmount) previousAmount = updatedTx.txInfo.tx.txOut.head.amount } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index c5fa39956b..1dc458a684 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.{BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.BitcoindService @@ -30,13 +30,14 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.MempoolTx import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, UpdateConfirmationTarget} import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ -import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -45,7 +46,7 @@ import java.util.concurrent.atomic.AtomicLong import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt -class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ChannelStateTestsHelperMethods with BeforeAndAfterAll { +class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ChannelStateTestsBase with BeforeAndAfterAll { override def beforeAll(): Unit = { startBitcoind() @@ -130,7 +131,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletClient) val testTags = channelType match { - case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) + case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) case ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) case ChannelTypes.StaticRemoteKey => Set(ChannelStateTestsTags.StaticRemoteKey) case _ => Set.empty[String] @@ -159,7 +160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val publishCommitTx = alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) // Forward the anchor tx to the publisher. val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishAnchor.txInfo.input.outPoint.txid === commitTx.tx.txid) + assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.tx.txid) assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) val anchorTx = publishAnchor.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmBefore = overrideCommitTarget) @@ -167,7 +168,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -176,13 +177,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } test("commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -193,13 +194,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } test("commit tx feerate high enough and commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -211,13 +212,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } test("remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -229,13 +230,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } test("remote commit tx published, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -246,19 +247,19 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) // When the remote commit tx is still unconfirmed, we want to retry in case it is evicted from the mempool and our // commit is then published. - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } test("remote commit tx replaces local commit tx, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === FeeratePerKw(2500 sat)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate == FeeratePerKw(2500 sat)) // We lower the feerate to make it easy to replace our commit tx by theirs in the mempool. val lowFeerate = FeeratePerKw(500 sat) @@ -277,20 +278,20 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === WalletInputGone) + assert(result.cmd == anchorTx) + assert(result.reason == InputGone) // Since our wallet input is gone, we will retry and discover that a commit tx has been confirmed. val publisher2 = createPublisher() publisher2 ! Publish(probe.ref, anchorTx) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === anchorTx) - assert(result2.reason === TxSkipped(retryNextBlock = false)) + assert(result2.cmd == anchorTx) + assert(result2.reason == TxSkipped(retryNextBlock = false)) } } test("funding tx not found, skipping anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -299,14 +300,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val cmd = anchorTx.copy(commitments = anchorTx.commitments.copy(commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(0 sat, Nil), Nil))) publisher ! Publish(probe.ref, cmd) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === cmd) + assert(result.cmd == cmd) // We should keep retrying until the funding transaction is available. - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } test("not enough funds to increase commit tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // close channel and wait for the commit tx to be published, anchor will not be published because we don't have enough funds @@ -317,21 +318,21 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(25_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) // When the remote commit tx is still unconfirmed, we want to retry in case it is evicted from the mempool and our // commit is then published. - assert(result.reason === CouldNotFund) + assert(result.reason == CouldNotFund) } } test("commit tx feerate too low, spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) - assert(getMempool().length === 1) + assert(getMempool().length == 1) val targetFeerate = FeeratePerKw(3000 sat) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target what's provided. @@ -348,14 +349,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) } } test("commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 32) @@ -376,7 +377,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) } @@ -391,7 +392,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 22000 sat, 15000 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. @@ -411,7 +412,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(result.tx.txIn.length > 2) // we added more than 1 wallet input assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) @@ -419,7 +420,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx fees not increased when confirmation target is far and feerate hasn't changed") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -437,33 +438,40 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) probe.expectNoMessage(500 millis) val mempoolTxs2 = getMempool() - assert(mempoolTxs.map(_.txid).toSet === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs.map(_.txid).toSet == mempoolTxs2.map(_.txid).toSet) } } test("commit tx not confirming, lowering anchor output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val oldFeerate = FeeratePerKw(3000 sat) setFeerate(oldFeerate) publisher ! Publish(probe.ref, anchorTx) // wait for the commit tx and anchor tx to be published + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx1.txid == anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. val newFeerate = FeeratePerKw(5000 sat) setFeerate(newFeerate, blockTarget = 12) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) - awaitCond(!isInMempool(mempoolAnchorTx1.txid), interval = 200 millis, max = 30 seconds) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(mempoolAnchorTx1.txid)) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx2.txid == anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee = Transactions.weight2fee(newFeerate, mempoolTxs2.map(_.weight).sum.toInt) @@ -473,27 +481,34 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + // The feerate is (much) higher for higher block targets val targetFeerate = FeeratePerKw(75_000 sat) setFeerate(FeeratePerKw(3000 sat)) setFeerate(targetFeerate, blockTarget = 6) publisher ! Publish(probe.ref, anchorTx) // wait for the commit tx and anchor tx to be published + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val anchorTx1 = getMempool().filter(_.txid != commitTx.tx.txid).head + assert(anchorTx1.txid == anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - awaitCond(!isInMempool(anchorTx1.txid), interval = 200 millis, max = 30 seconds) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(anchorTx1.txid)) val anchorTx2 = getMempool().filter(_.txid != commitTx.tx.txid).head + assert(anchorTx2.txid == anchorTxId2) // We used different inputs to be able to bump to the desired feerate. assert(anchorTx1.txIn.map(_.outPoint).toSet != anchorTx2.txIn.map(_.outPoint).toSet) @@ -505,7 +520,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, not enough funds to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -523,15 +538,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // A new block is found, and the feerate has increased for our block target, but we don't have enough funds to bump the fees. system.eventStream.subscribe(probe.ref, classOf[NotifyNodeOperator]) + // just making sure that we have been subscribed to the event, otherwise there is a possible race condition + awaitCond({ + system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Info, "ping")) + probe.msgAvailable + }, max = 30 seconds) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - probe.expectMsgType[NotifyNodeOperator] + probe.fishForMessage() { + case nno: NotifyNodeOperator => nno.severity != NotificationsLogger.Info + case _ => false + } val mempoolTxs2 = getMempool() - assert(mempoolTxs1.map(_.txid).toSet === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs1.map(_.txid).toSet == mempoolTxs2.map(_.txid).toSet) } } test("commit tx not confirming, cannot use new unconfirmed inputs to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -557,27 +580,32 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) probe.expectMsgType[NotifyNodeOperator] val mempoolTxs2 = getMempool() - assert(mempoolTxs1.map(_.txid).toSet + walletTx.txid === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs1.map(_.txid).toSet + walletTx.txid == mempoolTxs2.map(_.txid).toSet) } } test("commit tx not confirming, updating confirmation target") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val feerateLow = FeeratePerKw(3000 sat) val feerateHigh = FeeratePerKw(5000 sat) setFeerate(feerateLow) setFeerate(feerateHigh, blockTarget = 6) // With the initial confirmation target, this will use the low feerate. publisher ! Publish(probe.ref, anchorTx) + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx1.txid == anchorTxId1) val targetFee1 = Transactions.weight2fee(feerateLow, mempoolTxs1.map(_.weight).sum.toInt) val actualFee1 = mempoolTxs1.map(_.fees).sum assert(targetFee1 * 0.9 <= actualFee1 && actualFee1 <= targetFee1 * 1.1, s"actualFee=$actualFee1 targetFee=$targetFee1") @@ -586,9 +614,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // We should now use the high feerate, which corresponds to that new target. publisher ! UpdateConfirmationTarget(aliceBlockHeight() + 15) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight())) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid awaitCond(!isInMempool(mempoolAnchorTx1.txid), interval = 200 millis, max = 30 seconds) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx2.txid == anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee2 = Transactions.weight2fee(feerateHigh, mempoolTxs2.map(_.weight).sum.toInt) @@ -598,7 +628,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when anchor tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -615,7 +645,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val publisher2 = createPublisher() publisher2 ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.reason === ConflictingTxUnconfirmed) + assert(result.reason == ConflictingTxUnconfirmed) getMempoolTxs(2) // the previous anchor tx and the commit tx are still in the mempool // our parent will stop us when receiving the TxRejected message. @@ -625,12 +655,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // the first publishing attempt succeeds generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - assert(probe.expectMsgType[TxConfirmed].cmd === anchorTx) + assert(probe.expectMsgType[TxConfirmed].cmd == anchorTx) } } test("unlock anchor utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -649,7 +679,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not publishing htlc tx") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -682,15 +712,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === htlcSuccess) - assert(result1.reason === ConflictingTxConfirmed) + assert(result1.cmd == htlcSuccess) + assert(result1.reason == ConflictingTxConfirmed) htlcSuccessPublisher ! Stop val htlcTimeoutPublisher = createPublisher() htlcTimeoutPublisher ! Publish(probe.ref, htlcTimeout) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === htlcTimeout) - assert(result2.reason === ConflictingTxConfirmed) + assert(result2.cmd == htlcTimeout) + assert(result2.reason == ConflictingTxConfirmed) htlcTimeoutPublisher ! Stop } } @@ -708,7 +738,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel and verify txs sent to watcher. val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) - assert(commitTx.tx.txOut.size === 6) + assert(commitTx.tx.txOut.size == 6) probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] @@ -738,7 +768,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase htlc tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight()) @@ -749,8 +779,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === htlcSuccess) - assert(result.reason === CouldNotFund) + assert(result.cmd == htlcSuccess) + assert(result.reason == CouldNotFund) htlcSuccessPublisher ! Stop } } @@ -769,7 +799,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val htlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(htlcSuccessResult.cmd === htlcSuccess) + assert(htlcSuccessResult.cmd == htlcSuccess) assert(htlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(commitTx.txid)) htlcSuccessPublisher ! Stop htlcSuccessResult.tx @@ -798,7 +828,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val htlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(htlcTimeoutResult.cmd === htlcTimeout) + assert(htlcTimeoutResult.cmd == htlcTimeout) assert(htlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(commitTx.txid)) htlcTimeoutPublisher ! Stop htlcTimeoutResult.tx @@ -813,10 +843,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(currentFeerate) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, currentFeerate) assert(htlcSuccess.txInfo.fee > 0.sat) - assert(htlcSuccessTx.txIn.length === 1) + assert(htlcSuccessTx.txIn.length == 1) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, currentFeerate) assert(htlcTimeout.txInfo.fee > 0.sat) - assert(htlcTimeoutTx.txIn.length === 1) + assert(htlcTimeoutTx.txIn.length == 1) } } @@ -836,34 +866,34 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) - assert(htlcSuccess.txInfo.fee === 0.sat) + assert(htlcSuccess.txInfo.fee == 0.sat) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) assert(htlcSuccessTx.txIn.length > 1) - assert(htlcTimeout.txInfo.fee === 0.sat) + assert(htlcTimeout.txInfo.fee == 0.sat) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) assert(htlcTimeoutTx.txIn.length > 1) } } test("htlc tx feerate zero, high commit feerate, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate val targetFeerate = commitFeerate / 2 val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) setFeerate(targetFeerate) - assert(htlcSuccess.txInfo.fee === 0.sat) + assert(htlcSuccess.txInfo.fee == 0.sat) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) assert(htlcSuccessTx.txIn.length > 1) - assert(htlcTimeout.txInfo.fee === 0.sat) + assert(htlcTimeout.txInfo.fee == 0.sat) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) assert(htlcTimeoutTx.txIn.length > 1) } @@ -885,7 +915,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 5200 sat, 5100 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(8_000 sat) @@ -900,56 +930,70 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, lowering output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx1.txid == htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(25_000 sat) setFeerate(targetFeerate, blockTarget = 6) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - awaitCond(!isInMempool(htlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) + val htlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcSuccessTx1.txid)) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx2.txid == htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) - assert(htlcSuccessInputs1 === htlcSuccessInputs2) + assert(htlcSuccessInputs1 == htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.4, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee") } } test("htlc success tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 15) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx1.txid == htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(75_000 sat) setFeerate(targetFeerate, blockTarget = 2) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 10)) + val htlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid awaitCond(!isInMempool(htlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx2.txid == htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) assert(htlcSuccessInputs1 !== htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) @@ -958,24 +1002,31 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx confirmation target reached, increasing fees") { - withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(10_000 sat) setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 6) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid var htlcSuccessTx = getMempoolTxs(1).head + assert(htlcSuccessTx.txid == htlcSuccessTxId) // We are only 6 blocks away from the confirmation target, so we bump the fees at each new block. (1 to 3).foreach(i => { system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + i)) - awaitCond(!isInMempool(htlcSuccessTx.txid), interval = 200 millis, max = 30 seconds) + val bumpedHtlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcSuccessTx.txid)) val bumpedHtlcSuccessTx = getMempoolTxs(1).head + assert(bumpedHtlcSuccessTx.txid == bumpedHtlcSuccessTxId) assert(htlcSuccessTx.fees < bumpedHtlcSuccessTx.fees) htlcSuccessTx = bumpedHtlcSuccessTx }) @@ -983,7 +1034,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc timeout tx not confirming, increasing fees") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val feerate = FeeratePerKw(15_000 sat) @@ -991,22 +1042,29 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // The confirmation target for htlc-timeout corresponds to their CLTV: we should claim them asap once the htlc has timed out. val (commitTx, _, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcTimeoutPublisher = createPublisher() htlcTimeoutPublisher ! Publish(probe.ref, htlcTimeout) generateBlocks(144) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) + val htlcTimeoutTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcTimeoutTx1 = getMempoolTxs(1).head val htlcTimeoutInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcTimeoutTx1.txid == htlcTimeoutTxId1) // A new block is found, and we've already reached the confirmation target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) - awaitCond(!isInMempool(htlcTimeoutTx1.txid), interval = 200 millis, max = 30 seconds) + val htlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcTimeoutTx1.txid)) val htlcTimeoutTx2 = getMempoolTxs(1).head val htlcTimeoutInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcTimeoutTx2.txid == htlcTimeoutTxId2) assert(htlcTimeoutTx1.fees < htlcTimeoutTx2.fees) - assert(htlcTimeoutInputs1 === htlcTimeoutInputs2) + assert(htlcTimeoutInputs1 == htlcTimeoutInputs2) // Once the confirmation target is reach, we should raise the feerate by at least 20% at every block. val htlcTimeoutTargetFee = Transactions.weight2fee(feerate * 1.2, htlcTimeoutTx2.weight.toInt) assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx2.fees && htlcTimeoutTx2.fees <= htlcTimeoutTargetFee * 1.1, s"actualFee=${htlcTimeoutTx2.fees} targetFee=$htlcTimeoutTargetFee") @@ -1014,7 +1072,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("utxos count too low, setting short confirmation target") { - withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) @@ -1036,7 +1094,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when htlc tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -1055,7 +1113,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w2 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w2.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) val result = probe.expectMsgType[TxRejected] - assert(result.reason === ConflictingTxUnconfirmed) + assert(result.reason == ConflictingTxUnconfirmed) getMempoolTxs(1) // the previous htlc-success tx is still in the mempool // our parent will stop us when receiving the TxRejected message. @@ -1065,13 +1123,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // the first publishing attempt succeeds generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - assert(probe.expectMsgType[TxConfirmed].cmd === htlcSuccess) + assert(probe.expectMsgType[TxConfirmed].cmd == htlcSuccess) publisher1 ! Stop } } test("unlock htlc utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ setFeerate(FeeratePerKw(5_000 sat)) @@ -1088,7 +1146,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("local commit tx confirmed, not publishing claim htlc tx") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -1102,7 +1160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel. val localCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(remoteCommitTx.tx.txOut.size === 6) + assert(remoteCommitTx.tx.txOut.size == 6) probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] @@ -1120,15 +1178,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === claimHtlcSuccess) - assert(result1.reason === ConflictingTxConfirmed) + assert(result1.cmd == claimHtlcSuccess) + assert(result1.reason == ConflictingTxConfirmed) claimHtlcSuccessPublisher ! Stop val claimHtlcTimeoutPublisher = createPublisher() claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === claimHtlcTimeout) - assert(result2.reason === ConflictingTxConfirmed) + assert(result2.cmd == claimHtlcTimeout) + assert(result2.reason == ConflictingTxConfirmed) claimHtlcTimeoutPublisher ! Stop } } @@ -1147,9 +1205,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel and verify txs sent to watcher. val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) if (bob.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat == DefaultCommitmentFormat) { - assert(remoteCommitTx.tx.txOut.size === 4) + assert(remoteCommitTx.tx.txOut.size == 4) } else { - assert(remoteCommitTx.tx.txOut.size === 6) + assert(remoteCommitTx.tx.txOut.size == 6) } probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) @@ -1189,7 +1247,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcSuccessResult.cmd === claimHtlcSuccess) + assert(claimHtlcSuccessResult.cmd == claimHtlcSuccess) assert(claimHtlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcSuccessPublisher ! Stop claimHtlcSuccessResult.tx @@ -1218,24 +1276,24 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcTimeoutResult.cmd === claimHtlcTimeout) + assert(claimHtlcTimeoutResult.cmd == claimHtlcTimeout) assert(claimHtlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcTimeoutPublisher ! Stop claimHtlcTimeoutResult.tx } test("claim htlc tx feerate high enough, not changing output amount") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val currentFeerate = alice.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(2) val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 50) val claimHtlcSuccessTx = testPublishClaimHtlcSuccess(f, remoteCommitTx, claimHtlcSuccess, currentFeerate) assert(claimHtlcSuccess.txInfo.fee > 0.sat) - assert(claimHtlcSuccessTx.txIn.length === 1) + assert(claimHtlcSuccessTx.txIn.length == 1) val claimHtlcTimeoutTx = testPublishClaimHtlcTimeout(f, remoteCommitTx, claimHtlcTimeout, currentFeerate) assert(claimHtlcTimeout.txInfo.fee > 0.sat) - assert(claimHtlcTimeoutTx.txIn.length === 1) + assert(claimHtlcTimeoutTx.txIn.length == 1) } } @@ -1248,12 +1306,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) val claimHtlcSuccessTx = testPublishClaimHtlcSuccess(f, remoteCommitTx, claimHtlcSuccess, targetFeerate) - assert(claimHtlcSuccessTx.txIn.length === 1) - assert(claimHtlcSuccessTx.txOut.length === 1) + assert(claimHtlcSuccessTx.txIn.length == 1) + assert(claimHtlcSuccessTx.txOut.length == 1) assert(claimHtlcSuccessTx.txOut.head.amount < claimHtlcSuccess.txInfo.tx.txOut.head.amount) val claimHtlcTimeoutTx = testPublishClaimHtlcTimeout(f, remoteCommitTx, claimHtlcTimeout, targetFeerate) - assert(claimHtlcTimeoutTx.txIn.length === 1) - assert(claimHtlcTimeoutTx.txOut.length === 1) + assert(claimHtlcTimeoutTx.txIn.length == 1) + assert(claimHtlcTimeoutTx.txOut.length == 1) assert(claimHtlcTimeoutTx.txOut.head.amount < claimHtlcTimeout.txInfo.tx.txOut.head.amount) } } @@ -1275,7 +1333,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcSuccessResult.cmd === claimHtlcSuccess) + assert(claimHtlcSuccessResult.cmd == claimHtlcSuccess) assert(claimHtlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcSuccessPublisher ! Stop @@ -1292,7 +1350,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcTimeoutResult.cmd === claimHtlcTimeout) + assert(claimHtlcTimeoutResult.cmd == claimHtlcTimeout) assert(claimHtlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcTimeoutPublisher ! Stop } @@ -1310,8 +1368,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w1 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w1.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === claimHtlcSuccess) - assert(result1.reason === TxSkipped(retryNextBlock = true)) + assert(result1.cmd == claimHtlcSuccess) + assert(result1.reason == TxSkipped(retryNextBlock = true)) claimHtlcSuccessPublisher ! Stop val claimHtlcTimeoutPublisher = createPublisher() @@ -1321,8 +1379,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w2 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w2.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === claimHtlcTimeout) - assert(result2.reason === TxSkipped(retryNextBlock = true)) + assert(result2.cmd == claimHtlcTimeout) + assert(result2.reason == TxSkipped(retryNextBlock = true)) claimHtlcTimeoutPublisher ! Stop } } @@ -1336,23 +1394,29 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 144) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + // The Claim-HTLC-success tx will be immediately published. setFeerate(initialFeerate) val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val claimHtlcSuccessTx1 = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) - awaitCond(!isInMempool(claimHtlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) + val claimHtlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(claimHtlcSuccessTx1.txid)) val claimHtlcSuccessTx2 = getMempoolTxs(1).head + assert(claimHtlcSuccessTx2.txid == claimHtlcSuccessTxId2) assert(claimHtlcSuccessTx1.fees < claimHtlcSuccessTx2.fees) val targetHtlcSuccessFee = Transactions.weight2fee(targetFeerate, claimHtlcSuccessTx2.weight.toInt) assert(targetHtlcSuccessFee * 0.9 <= claimHtlcSuccessTx2.fees && claimHtlcSuccessTx2.fees <= targetHtlcSuccessFee * 1.1, s"actualFee=${claimHtlcSuccessTx2.fees} targetFee=$targetHtlcSuccessFee") val finalHtlcSuccessTx = getMempool().head - assert(finalHtlcSuccessTx.txIn.length === 1) - assert(finalHtlcSuccessTx.txOut.length === 1) - assert(finalHtlcSuccessTx.txIn.head.outPoint.txid === remoteCommitTx.txid) + assert(finalHtlcSuccessTx.txIn.length == 1) + assert(finalHtlcSuccessTx.txOut.length == 1) + assert(finalHtlcSuccessTx.txIn.head.outPoint.txid == remoteCommitTx.txid) // The Claim-HTLC-timeout will be published after the timeout. setFeerate(initialFeerate) @@ -1360,20 +1424,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) generateBlocks(144) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 144)) - assert(probe.expectMsgType[TxConfirmed].tx.txid === finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed + assert(probe.expectMsgType[TxConfirmed].tx.txid == finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed val claimHtlcTimeoutTx1 = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcTimeoutTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) - awaitCond(!isInMempool(claimHtlcTimeoutTx1.txid), interval = 200 millis, max = 30 seconds) + val claimHtlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(claimHtlcTimeoutTx1.txid)) val claimHtlcTimeoutTx2 = getMempoolTxs(1).head + assert(claimHtlcTimeoutTx2.txid == claimHtlcTimeoutTxId2) assert(claimHtlcTimeoutTx1.fees < claimHtlcTimeoutTx2.fees) val targetHtlcTimeoutFee = Transactions.weight2fee(targetFeerate, claimHtlcTimeoutTx2.weight.toInt) assert(targetHtlcTimeoutFee * 0.9 <= claimHtlcTimeoutTx2.fees && claimHtlcTimeoutTx2.fees <= targetHtlcTimeoutFee * 1.1, s"actualFee=${claimHtlcTimeoutTx2.fees} targetFee=$targetHtlcTimeoutFee") val finalHtlcTimeoutTx = getMempool().head - assert(finalHtlcTimeoutTx.txIn.length === 1) - assert(finalHtlcTimeoutTx.txOut.length === 1) - assert(finalHtlcTimeoutTx.txIn.head.outPoint.txid === remoteCommitTx.txid) + assert(finalHtlcTimeoutTx.txIn.length == 1) + assert(finalHtlcTimeoutTx.txOut.length == 1) + assert(finalHtlcTimeoutTx.txIn.head.outPoint.txid == remoteCommitTx.txid) } } @@ -1383,19 +1450,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (remoteCommitTx, claimHtlcSuccess, _) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 300) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + setFeerate(FeeratePerKw(5_000 sat)) val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val w1 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w1.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val claimHtlcSuccessTx = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx.txid) // New blocks are found and the feerate is higher, but the htlc would become dust, so we don't bump the fees. setFeerate(FeeratePerKw(50_000 sat)) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) probe.expectNoMessage(500 millis) val mempoolTxs = getMempool() - assert(mempoolTxs.map(_.txid).toSet === Set(claimHtlcSuccessTx.txid)) + assert(mempoolTxs.map(_.txid).toSet == Set(claimHtlcSuccessTx.txid)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index add2d7908c..5ad93f2b3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.channel.publish import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ @@ -75,7 +75,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val child = factory.expectMsgType[FinalTxPublisherSpawned].actor - assert(child.expectMsgType[FinalTxPublisher.Publish].cmd === cmd) + assert(child.expectMsgType[FinalTxPublisher.Publish].cmd == cmd) } test("publish final tx duplicate") { f => @@ -107,7 +107,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor val p = child.expectMsgType[ReplaceableTxPublisher.Publish] - assert(p.cmd === cmd) + assert(p.cmd == cmd) } test("publish replaceable tx duplicate") { f => @@ -119,7 +119,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val cmd = PublishReplaceableTx(anchorTx, null) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor - assert(child.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(child.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) // We ignore duplicates that don't use a more aggressive confirmation target: txPublisher ! PublishReplaceableTx(anchorTx, null) @@ -186,13 +186,33 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish] - txPublisher ! TxRejected(attempt2.id, cmd2, WalletInputGone) + txPublisher ! TxRejected(attempt2.id, cmd2, InputGone) attempt2.actor.expectMsg(ReplaceableTxPublisher.Stop) attempt1.actor.expectNoMessage(100 millis) // this error doesn't impact other publishing attempts // We automatically retry the failed attempt with new wallet inputs: val attempt3 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd2) + assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd2) + } + + test("publishing attempt fails (main input gone)") { f => + import f._ + + val input = OutPoint(randomBytes32(), 3) + val tx = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) + val cmd = PublishFinalTx(tx, input, "final-tx", 0 sat, None) + txPublisher ! cmd + val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] + attempt1.actor.expectMsgType[FinalTxPublisher.Publish] + + txPublisher ! TxRejected(attempt1.id, cmd, InputGone) + attempt1.actor.expectMsg(FinalTxPublisher.Stop) + + // We don't retry until a new block is found. + factory.expectNoMessage(100 millis) + system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) + val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned] + assert(attempt2.actor.expectMsgType[FinalTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (not enough funds)") { f => @@ -213,7 +233,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { factory.expectNoMessage(100 millis) system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (transaction skipped)") { f => @@ -239,7 +259,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) val attempt3 = factory.expectMsgType[FinalTxPublisherSpawned] - assert(attempt3.actor.expectMsgType[publish.FinalTxPublisher.Publish].cmd === cmd2) + assert(attempt3.actor.expectMsgType[publish.FinalTxPublisher.Publish].cmd == cmd2) factory.expectNoMessage(100 millis) } @@ -277,7 +297,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We retry when a new block is found: system.eventStream.publish(CurrentBlockHeight(nodeParams.currentBlockHeight + 1)) val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (confirmed conflicting transaction)") { f => @@ -314,14 +334,14 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { factory.expectNoMessage(100 millis) } - test("update publishing attempts") { _ => + test("update publishing attempts") { () => { // No attempts. val attempts = PublishAttempts.empty assert(attempts.isEmpty) - assert(attempts.count === 0) + assert(attempts.count == 0) assert(attempts.attempts.isEmpty) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) } { // Only final attempts. @@ -329,19 +349,19 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt2 = FinalAttempt(UUID.randomUUID(), null, null) val attempts = PublishAttempts.empty.add(attempt1).add(attempt2) assert(!attempts.isEmpty) - assert(attempts.count === 2) + assert(attempts.count == 2) assert(attempts.replaceableAttempt_opt.isEmpty) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) - assert(attempts.remove(attempt1.id) === (Seq(attempt1), PublishAttempts(Seq(attempt2), None))) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) + assert(attempts.remove(attempt1.id) == (Seq(attempt1), PublishAttempts(Seq(attempt2), None))) } { // Only replaceable attempts. val attempt = ReplaceableAttempt(UUID.randomUUID(), null, BlockHeight(0), null) val attempts = PublishAttempts(Nil, Some(attempt)) assert(!attempts.isEmpty) - assert(attempts.count === 1) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) - assert(attempts.remove(attempt.id) === (Seq(attempt), PublishAttempts.empty)) + assert(attempts.count == 1) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) + assert(attempts.remove(attempt.id) == (Seq(attempt), PublishAttempts.empty)) } { // Mix of final and replaceable attempts with the same id. @@ -350,9 +370,9 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt3 = FinalAttempt(UUID.randomUUID(), null, null) val attempts = PublishAttempts(Seq(attempt2), Some(attempt1)).add(attempt3) assert(!attempts.isEmpty) - assert(attempts.count === 3) - assert(attempts.remove(attempt3.id) === (Seq(attempt3), PublishAttempts(Seq(attempt2), Some(attempt1)))) - assert(attempts.remove(attempt1.id) === (Seq(attempt2, attempt1), PublishAttempts(Seq(attempt3), None))) + assert(attempts.count == 3) + assert(attempts.remove(attempt3.id) == (Seq(attempt3), PublishAttempts(Seq(attempt2), Some(attempt1)))) + assert(attempts.remove(attempt1.id) == (Seq(attempt2, attempt1), PublishAttempts(Seq(attempt3), None))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala index 3f0e55e671..0795a5dad1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchParentTxConfirmed, WatchParentTxConfirmedTriggered} import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext @@ -67,8 +67,8 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik monitor ! CheckTx(probe.ref, tx, "relative-delay") val w = watcher.expectMsgType[WatchParentTxConfirmed] - assert(w.txId === parentTx.txid) - assert(w.minDepth === 3) + assert(w.txId == parentTx.txid) + assert(w.minDepth == 3) probe.expectNoMessage(100 millis) w.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx) @@ -92,7 +92,7 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val w1 = watcher.expectMsgType[WatchParentTxConfirmed] val w2 = watcher.expectMsgType[WatchParentTxConfirmed] watcher.expectNoMessage(100 millis) - assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet === Set((parentTx1.txid, 3), (parentTx2.txid, 1))) + assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet == Set((parentTx1.txid, 3), (parentTx2.txid, 1))) probe.expectNoMessage(100 millis) w1.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx1) @@ -121,7 +121,7 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val w1 = watcher.expectMsgType[WatchParentTxConfirmed] val w2 = watcher.expectMsgType[WatchParentTxConfirmed] watcher.expectNoMessage(100 millis) - assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet === Set((parentTx1.txid, 3), (parentTx2.txid, 6))) + assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet == Set((parentTx1.txid, 3), (parentTx2.txid, 6))) probe.expectNoMessage(100 millis) w1.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index d9625321dc..82616501fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -17,45 +17,35 @@ package fr.acinq.eclair.channel.states import akka.actor.typed.scaladsl.adapter.actorRefAdapter -import akka.actor.{ActorContext, ActorRef} -import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} +import akka.actor.{ActorContext, ActorRef, ActorSystem} +import akka.testkit.{TestFSMRef, TestKit, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, ScriptFlags, Transaction} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeTargets, FeeratePerKw} import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import org.scalatest.{FixtureTestSuite, ParallelTestExecution} +import org.scalatest.Assertions +import org.scalatest.concurrent.Eventually import java.util.UUID +import scala.concurrent.Await import scala.concurrent.duration._ -/** - * Created by PM on 23/08/2016. - */ -trait ChannelStateTestsBase extends ChannelStateTestsHelperMethods with FixtureTestSuite with ParallelTestExecution { - - implicit class ChannelWithTestFeeConf(a: TestFSMRef[ChannelState, ChannelData, Channel]) { - // @formatter:off - def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] - def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets - // @formatter:on - } - -} - object ChannelStateTestsTags { /** If set, channels will use option_support_large_channel. */ val Wumbo = "wumbo" @@ -76,16 +66,22 @@ object ChannelStateTestsTags { /** If set, max-htlc-value-in-flight will be set to a low value for Alice. */ val AliceLowMaxHtlcValueInFlight = "alice_low_max_htlc_value_in_flight" /** If set, channels will use option_upfront_shutdown_script. */ - val OptionUpfrontShutdownScript = "option_upfront_shutdown_script" + val UpfrontShutdownScript = "option_upfront_shutdown_script" /** If set, Alice will have a much higher dust limit than Bob. */ val HighDustLimitDifferenceAliceBob = "high_dust_limit_difference_alice_bob" /** If set, Bob will have a much higher dust limit than Alice. */ val HighDustLimitDifferenceBobAlice = "high_dust_limit_difference_bob_alice" /** If set, channels will use option_channel_type. */ val ChannelType = "option_channel_type" + /** If set, channels will use option_zeroconf. */ + val ZeroConf = "zeroconf" + /** If set, channels will use option_scid_alias. */ + val ScidAlias = "scid_alias" + /** If set, we won't spend anchors to fee-bump commitments without htlcs (no funds at risk). */ + val DontSpendAnchorWithoutHtlcs = "dont-spend-anchor-without-htlcs" } -trait ChannelStateTestsHelperMethods extends TestKitBase { +trait ChannelStateTestsBase extends Assertions with Eventually { case class SetupFixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], @@ -95,8 +91,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe, - relayerA: TestProbe, - relayerB: TestProbe, + alice2relayer: TestProbe, + bob2relayer: TestProbe, channelUpdateListener: TestProbe, wallet: OnChainWallet, alicePeer: TestProbe, @@ -104,6 +100,20 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def currentBlockHeight: BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } + implicit class ChannelWithTestFeeConf(a: TestFSMRef[ChannelState, ChannelData, Channel]) { + // @formatter:off + def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets + // @formatter:on + } + + implicit val system: ActorSystem + val systemA: ActorSystem = ActorSystem("system-alice") + val systemB: ActorSystem = ActorSystem("system-bob") + + system.registerOnTermination(TestKit.shutdownActorSystem(systemA)) + system.registerOnTermination(TestKit.shutdownActorSystem(systemB)) + def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: OnChainWallet = new DummyOnChainWallet(), tags: Set[String] = Set.empty): SetupFixture = { val aliceOrigin = TestProbe() val alice2bob = TestProbe() @@ -114,28 +124,38 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { TestUtils.forwardOutgoingToPipe(bobPeer, bob2alice.ref) val alice2blockchain = TestProbe() val bob2blockchain = TestProbe() - val relayerA = TestProbe() - val relayerB = TestProbe() + val alice2relayer = TestProbe() + val bob2relayer = TestProbe() val channelUpdateListener = TestProbe() - system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) - system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) + systemA.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) + systemA.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) + systemB.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) + systemB.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) val router = TestProbe() val finalNodeParamsA = nodeParamsA - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) val finalNodeParamsB = nodeParamsB - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) - SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = { + implicit val system: ActorSystem = systemA + TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) + } + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = { + implicit val system: ActorSystem = systemB + TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + } + SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) } - def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = { + def computeFeatures(setup: SetupFixture, tags: Set[String], channelFlags: ChannelFlags): (LocalParams, LocalParams, SupportedChannelType) = { import setup._ val aliceInitFeatures = Alice.nodeParams.features @@ -144,30 +164,40 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) + .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) + .initFeatures() - val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) + val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) + + // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations + if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams .modify(_.initFeatures).setTo(aliceInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet))) + .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getReceivePubkey(), 10 seconds))) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000)) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) val bobParams = Bob.channelParams .modify(_.initFeatures).setTo(bobInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet))) + .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getReceivePubkey(), 10 seconds))) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) @@ -175,26 +205,29 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { (aliceParams, bobParams, channelType) } - def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { + def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty, interceptChannelUpdates: Boolean = true): Transaction = { import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) - val channelFlags = if (tags.contains(ChannelStateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty - val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) + val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) } else { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } + val eventListener = TestProbe() + systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) - assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) + assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] @@ -205,28 +238,38 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { bob2alice.forward(alice) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + val fundingTx = eventListener.expectMsgType[TransactionPublished].tx + if (!channelType.features.contains(Features.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } + eventually(assert(alice.stateName == WAIT_FOR_CHANNEL_READY)) + eventually(assert(bob.stateName == WAIT_FOR_CHANNEL_READY)) alice2blockchain.expectMsgType[WatchFundingLost] bob2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] + alice2bob.expectMsgType[ChannelReady] alice2bob.forward(bob) - bob2alice.expectMsgType[FundingLocked] + bob2alice.expectMsgType[ChannelReady] bob2alice.forward(alice) + if (interceptChannelUpdates) { + // we don't forward the channel updates, in reality they would be processed by the router + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + } alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.channelReserve).max(0 msat)) + eventually(assert(alice.stateName == NORMAL)) + eventually(assert(bob.stateName == NORMAL)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.requestedChannelReserve_opt.getOrElse(0 sat)).max(0 msat)) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] + fundingTx } def localOrigin(replyTo: ActorRef): Origin.LocalHot = Origin.LocalHot(replyTo, UUID.randomUUID()) @@ -246,7 +289,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) - val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None)).get._1.copy(commit = false) + val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, Some(randomBytes32()), None)).get._1.copy(commit = false) (paymentPreimage, cmd) } @@ -269,7 +312,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! cmdAdd val htlc = s2r.expectMsgType[UpdateAddHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(htlc)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(htlc))) htlc } @@ -277,21 +320,21 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! CMD_FULFILL_HTLC(id, preimage) val fulfill = s2r.expectMsgType[UpdateFulfillHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fulfill)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fulfill))) } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure)) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fail)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fail))) } def crossSign(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { val sender = TestProbe() - val sCommitIndex = s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index - val rCommitIndex = r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index - val rHasChanges = Commitments.localHasChanges(r.stateData.asInstanceOf[HasCommitments].commitments) + val sCommitIndex = s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index + val rCommitIndex = r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index + val rHasChanges = Commitments.localHasChanges(r.stateData.asInstanceOf[PersistentChannelData].commitments) s ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] s2r.expectMsgType[CommitSig] @@ -307,15 +350,19 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s2r.forward(r) r2s.expectMsgType[RevokeAndAck] r2s.forward(s) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == sCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1) + eventually { + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 2) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 2) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + } } else { - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == sCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1) + eventually { + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 1) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 1) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + } } } @@ -331,7 +378,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { r2s.forward(s) s2r.expectMsgType[RevokeAndAck] s2r.forward(r) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.spec.commitTxFeerate == feerate) + eventually(assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.spec.commitTxFeerate == feerate)) } def mutualClose(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, s2blockchain: TestProbe, r2blockchain: TestProbe): Unit = { @@ -354,8 +401,10 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s2blockchain.expectMsgType[WatchTxConfirmed] r2blockchain.expectMsgType[TxPublisher.PublishTx] r2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(s.stateName == CLOSING) - awaitCond(r.stateName == CLOSING) + eventually { + assert(s.stateName == CLOSING) + assert(r.stateName == CLOSING) + } // both nodes are now in CLOSING state with a mutual close tx pending for confirmation } @@ -368,7 +417,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val commitTx = localCommit.commitTxAndRemoteSig.commitTx.tx s ! Error(ByteVector32.Zeroes, "oops") - awaitCond(s.stateName == CLOSING) + eventually(assert(s.stateName == CLOSING)) val closingState = s.stateData.asInstanceOf[DATA_CLOSING] assert(closingState.localCommitPublished.isDefined) val localCommitPublished = closingState.localCommitPublished.get @@ -396,8 +445,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { } // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === commitTx.txid) - localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) + assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) + localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) // we watch outputs of the commitment tx that both parties may spend and anchor outputs val watchedOutputIndexes = localCommitPublished.htlcTxs.keySet.map(_.index) ++ localCommitPublished.claimAnchorTxs.collect { case tx: ClaimLocalAnchorOutputTx => tx.input.outPoint.index } @@ -413,7 +462,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): RemoteCommitPublished = { // we make s believe r unilaterally closed the channel s ! WatchFundingSpentTriggered(rCommitTx) - awaitCond(s.stateName == CLOSING) + eventually(assert(s.stateName == CLOSING)) val closingData = s.stateData.asInstanceOf[DATA_CLOSING] val remoteCommitPublished_opt = closingData.remoteCommitPublished.orElse(closingData.nextRemoteCommitPublished).orElse(closingData.futureRemoteCommitPublished) assert(remoteCommitPublished_opt.isDefined) @@ -432,8 +481,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { assert(publishedClaimHtlcTxs.map(_.input).toSet == claimHtlcTxs.map(_.input.outPoint).toSet) // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === rCommitTx.txid) - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) + assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == rCommitTx.txid) + remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) // we watch outputs of the commitment tx that both parties may spend val htlcOutputIndexes = remoteCommitPublished.claimHtlcTxs.keySet.map(_.index) @@ -458,7 +507,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { } -object ChannelStateTestsHelperMethods { +object ChannelStateTestsBase { case class FakeTxPublisherFactory(txPublisher: TestProbe) extends Channel.TxPublisherFactory { override def spawnTxPublisher(context: ActorContext, remoteNodeId: PublicKey): akka.actor.typed.ActorRef[TxPublisher.Command] = txPublisher.ref diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index cc5138ac00..aaac7e188a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -18,11 +18,12 @@ package fr.acinq.eclair.channel.states.a import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.NoOpOnChainWallet -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, TestConstants, TestKitBaseClass} @@ -44,25 +45,26 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) - .modify(_.maxRemoteDustLimit).setToIf(test.tags.contains("high-remote-dust-limit"))(15000 sat) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(test.tags.contains("high-remote-dust-limit"))(15000 sat) val bobNodeParams = Bob.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) val setup = init(aliceNodeParams, bobNodeParams, wallet = new NoOpOnChainWallet()) import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -75,8 +77,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(accept.upfrontShutdownScript_opt === Some(ByteVector.empty)) - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.upfrontShutdownScript_opt.contains(ByteVector.empty)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) aliceOrigin.expectNoMessage() @@ -85,39 +87,49 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + aliceOrigin.expectNoMessage() + } + + test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) // Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but // they both activated anchor outputs so it is the default choice anyway. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty)) alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) awaitCond(alice.stateName == CLOSED) @@ -128,17 +140,17 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Alice asked for a standard channel whereas they both support anchor outputs. - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.Standard) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.Standard) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) // Alice asked for a standard channel whereas they both support anchor outputs. Bob doesn't support explicit channel // type negotiation so Alice needs to abort because the channel types won't match. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) @@ -147,7 +159,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectMsgType[Status.Failure] } - test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { _ => + test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { () => val setup = init(Alice.nodeParams, Bob.nodeParams, wallet = new NoOpOnChainWallet()) import setup._ @@ -155,23 +167,23 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Bob advertises support for anchor outputs, but Alice doesn't. val aliceParams = Alice.channelParams val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Empty, channelConfig, ChannelTypes.AnchorOutputs) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob, open) val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (invalid channel type)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) val invalidAccept = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))) bob2alice.forward(alice, invalidAccept) alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) @@ -186,7 +198,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val invalidMaxAcceptedHtlcs = 484 alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -198,7 +210,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val lowDustLimitSatoshis = 353.sat alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUST_LIMIT).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUST_LIMIT).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -209,7 +221,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val highDustLimitSatoshis = 2000.sat alice ! accept.copy(dustLimitSatoshis = highDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.maxRemoteDustLimit).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -220,7 +232,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val delayTooHigh = CltvExpiryDelta(10000) alice ! accept.copy(toSelfDelay = delayTooHigh) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelay).getMessage)) + assert(error == Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -232,7 +244,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooHigh = TestConstants.fundingSatoshis * 0.3 alice ! accept.copy(channelReserveSatoshis = reserveTooHigh) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) + assert(error == Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -243,7 +255,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooSmall = accept.dustLimitSatoshis - 1.sat alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -255,7 +267,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooSmall = open.dustLimitSatoshis - 1.sat alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) + assert(error == Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -267,7 +279,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val dustTooBig = open.channelReserveSatoshis + 1.sat alice ! accept.copy(dustLimitSatoshis = dustTooBig) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -281,7 +293,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) @@ -291,7 +303,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) @@ -302,7 +314,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef")))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index caeb6da5a5..f9692f40f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -17,10 +17,11 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} @@ -43,19 +44,20 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import com.softwaremill.quicklens._ val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) val setup = init(nodeParamsB = bobNodeParams) import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) @@ -66,39 +68,48 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import f._ val open = alice2bob.expectMsgType[OpenChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(open.upfrontShutdownScript_opt === Some(ByteVector.empty)) + assert(open.upfrontShutdownScript_opt.contains(ByteVector.empty)) // We always send a channel type, even for standard channels. - assert(open.channelType_opt === Some(ChannelTypes.Standard)) + assert(open.channelType_opt.contains(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) } test("recv OpenChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputs) } test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + } + + test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) } test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.Standard)) + assert(open.channelType_opt.contains(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) } test("recv OpenChannel (invalid chain)") { f => @@ -108,17 +119,29 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val livenetChainHash = Block.LivenetGenesisBlock.hash bob ! open.copy(chainHash = livenetChainHash) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenChannel (funding too low, public channel)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val lowFunding = 100.sat + val announceChannel = true + bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) + val error = bob2alice.expectMsgType[Error] + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (funding too low)") { f => + test("recv OpenChannel (funding too low, private channel)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val lowFunding = 100.sat - bob ! open.copy(fundingSatoshis = lowFunding) + val announceChannel = false + bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -128,17 +151,17 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingMsat = 100000000.sat bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage).toAscii) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (fundingSatoshis > max-funding-satoshis)", Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val highFundingSat = Bob.nodeParams.maxFundingSatoshis + Btc(1) + val highFundingSat = Bob.nodeParams.channelConf.maxFundingSatoshis + Btc(1) bob ! open.copy(fundingSatoshis = highFundingSat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage).toAscii) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) } test("recv OpenChannel (invalid max accepted htlcs)") { f => @@ -147,7 +170,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -157,7 +180,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val invalidPushMsat = 100000000000L.msat bob ! open.copy(pushMsat = invalidPushMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -167,7 +190,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val delayTooHigh = CltvExpiryDelta(10000) bob ! open.copy(toSelfDelay = delayTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelay).getMessage)) + assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -178,7 +201,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val reserveTooHigh = TestConstants.fundingSatoshis * 0.3 bob ! open.copy(channelReserveSatoshis = reserveTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) + assert(error == Error(open.temporaryChannelId, ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -190,7 +213,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(feeratePerKw = tinyFee) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000")) + assert(error == Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000")) awaitCond(bob.stateName == CLOSED) } @@ -202,7 +225,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(feeratePerKw = tinyFee) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252")) + assert(error == Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252")) awaitCond(bob.stateName == CLOSED) } @@ -213,7 +236,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(channelReserveSatoshis = reserveTooSmall) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage)) + assert(error == Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -223,7 +246,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val dustLimitTooHigh = 2000.sat bob ! open.copy(dustLimitSatoshis = dustLimitTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.maxRemoteDustLimit).getMessage)) + assert(error == Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -235,7 +258,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(fundingSatoshis = fundingSatoshis, pushMsat = pushMsat) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, pushMsat, (open.channelReserveSatoshis - 1.sat).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, pushMsat, (open.channelReserveSatoshis - 1.sat).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -248,7 +271,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) } - test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.defaultFinalScriptPubKey)) @@ -257,7 +280,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript == open.upfrontShutdownScript_opt) } - test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) @@ -266,7 +289,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript.isEmpty) } - test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef")))) @@ -274,6 +297,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } + test("recv OpenChannel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob, open) + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.minimumDepth == 0) + } + test("recv Error") { f => import f._ bob ! Error(ByteVector32.Zeroes, "oops") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 6c2819db0c..ff298ee255 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -18,10 +18,11 @@ package fr.acinq.eclair.channel.states.b import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions @@ -43,9 +44,9 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) { (1000100 sat, (1000000 sat).toMilliSatoshi) // toLocal = 100 satoshis @@ -59,11 +60,12 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -85,7 +87,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) } test("recv FundingCreated (wumbo)", Tag(ChannelStateTestsTags.Wumbo)) { f => @@ -98,19 +100,19 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun bob2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] // when we are fundee, we use a higher min depth for wumbo channels - assert(watchConfirmed.minDepth > Bob.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth > Bob.nodeParams.channelConf.minDepthBlocks) } test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f => import f._ val fees = Transactions.weight2fee(TestConstants.feeratePerKw, Transactions.DefaultCommitmentFormat.commitWeight) val bobParams = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams - val reserve = bobParams.channelReserve + val reserve = bobParams.requestedChannelReserve_opt.get val missing = 100.sat - fees - reserve val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) val error = bob2alice.expectMsgType[Error] - assert(error === Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missing=${-missing} reserve=$reserve fees=$fees")) + assert(error == Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missing=${-missing} reserve=$reserve fees=$fees")) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 93e1684c8f..23419edfda 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -18,10 +18,11 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.blockchain.NoOpOnChainWallet -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{TestConstants, TestKitBaseClass} @@ -42,11 +43,12 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val setup = init(wallet = new NoOpOnChainWallet()) import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 1a8d538b9d..ddccfb0499 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -18,12 +18,13 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} @@ -44,9 +45,9 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val (fundingSatoshis, pushMsat) = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) { (Btc(5).toSatoshi, TestConstants.pushMsat) @@ -58,11 +59,12 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -81,16 +83,28 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv FundingSigned with valid signature") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) val fundingTxId = alice2blockchain.expectMsgType[WatchFundingSpent].txId val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx.txid === fundingTxId) + assert(txPublished.tx.txid == fundingTxId) assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] + } + + test("recv FundingSigned with valid signature (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + // alice doesn't watch for the funding tx to confirm + alice2blockchain.expectMsgType[WatchFundingSpent] + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectNoMessage(100 millis) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } @@ -101,8 +115,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) alice2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - // when we are funder, we keep our regular min depth even for wumbo channels - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala new file mode 100644 index 0000000000..e08f1c3879 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -0,0 +1,293 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.c + +import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.publish.TxPublisher +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration._ + +/** + * Created by PM on 05/07/2016. + */ + +class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + val relayFees: RelayFees = RelayFees(999 msat, 1234) + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + + within(30 seconds) { + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob2blockchain.expectMsgType[WatchFundingSpent] + if (!test.tags.contains(ChannelStateTestsTags.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } + alice2blockchain.expectMsgType[WatchFundingLost] + bob2blockchain.expectMsgType[WatchFundingLost] + alice2bob.expectMsgType[ChannelReady] + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) + } + } + + test("recv ChannelReady") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real.isInstanceOf[RealScidStatus.Temporary]) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) + val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) + bob2alice.forward(alice) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // we have a real scid, but the channel is not announced so alice uses bob's alias + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (no alias)") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + val realScid = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // the channel is not announced but bob didn't send an alias so we use the real scid + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == realScid) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) + bob2alice.forward(alice) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is 0-conf but bob didn't provide an alias: it's a spec violation, so we use our local alias and if + // they can't understand it, too bad for them + assert(channelUpdateSentToPeer.shortChannelId == aliceIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (public)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) + bob2alice.forward(alice) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) + bob2alice.forward(alice) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv WatchFundingSpentTriggered (remote commit)") { f => + import f._ + // bob publishes his commitment tx + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(tx) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + awaitCond(alice.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! Error(ByteVector32.Zeroes, "oops") + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + awaitCond(bob.stateName == CLOSING) + assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_CHANNEL_READY))) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + val sender = TestProbe() + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! CMD_FORCECLOSE(sender.ref) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index ab3a5ed684..340f7ca24f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -17,18 +17,19 @@ package fr.acinq.eclair.channel.states.c import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsBase +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 -import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.scalatest.Outcome +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -38,7 +39,7 @@ import scala.concurrent.duration._ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { @@ -46,13 +47,16 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -65,45 +69,99 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[WatchFundingSpent] alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] listener.expectMsgType[TransactionPublished] // alice has published the funding transaction awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, listener))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, listener))) } } - test("recv FundingLocked") { f => + test("recv ChannelReady (funder, with remote alias)") { f => import f._ - // we create a new listener that registers after alice has published the funding tx - val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) - // make bob send a FundingLocked msg + // make bob send a ChannelReady msg val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx === fundingTx) - assert(txPublished.miningFee === 0.sat) // bob is fundee - assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) - val msg = bob2alice.expectMsgType[FundingLocked] + val bobChannelReady = bob2alice.expectMsgType[ChannelReady] + assert(bobChannelReady.alias_opt.isDefined) + // test starts here bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(msg)) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + // alice stops waiting for confirmations since bob is accepting the channel + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] + assert(aliceChannelReady.alias_opt.nonEmpty) + awaitAssert(assert(alice.stateName == NORMAL)) + } + + test("recv ChannelReady (funder, no remote alias)") { f => + import f._ + // make bob send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val channelReadyNoAlias = bob2alice.expectMsgType[ChannelReady].copy(tlvStream = TlvStream.empty) + // test starts here + bob2alice.forward(alice, channelReadyNoAlias) + // alice keeps bob's channel_ready for later processing + eventually { + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReadyNoAlias)) + } + alice2blockchain.expectNoMessage() + } + + test("recv ChannelReady (fundee)") { f => + import f._ + // make alice send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val channelReady = alice2bob.expectMsgType[ChannelReady] + // test starts here + alice2bob.forward(bob) + // alice keeps bob's channel_ready for later processing + eventually { + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReady)) + } + // bob is fundee, he doesn't trust alice and won't create a zero-conf watch + bob2blockchain.expectNoMessage() } - test("recv WatchFundingConfirmedTriggered") { f => + test("recv WatchFundingConfirmedTriggered (funder)") { f => import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) alice2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] + val channelReady = alice2bob.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) + } + + test("recv WatchFundingConfirmedTriggered (fundee)") { f => + import f._ + // we create a new listener that registers after alice has published the funding tx + val listener = TestProbe() + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + // make bob send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val txPublished = listener.expectMsgType[TransactionPublished] + assert(txPublished.tx == fundingTx) + assert(txPublished.miningFee == 0.sat) // bob is fundee + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + bob2blockchain.expectMsgType[WatchFundingLost] + val channelReady = bob2alice.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) } test("recv WatchFundingConfirmedTriggered (bad funding pubkey script)") { f => @@ -176,7 +234,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF awaitCond(bob.stateName == OFFLINE) // We reset the waiting period to the current block height when starting up after updating eclair. val currentBlockHeight = bob.underlyingActor.nodeParams.currentBlockHeight - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince === currentBlockHeight) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince == currentBlockHeight) } test("recv WatchFundingSpentTriggered (remote commit)") { f => @@ -185,7 +243,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(tx) alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) awaitCond(alice.stateName == CLOSING) } @@ -194,7 +252,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } @@ -203,9 +261,16 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake + awaitCond(bob.stateName == CLOSED) } test("recv CMD_CLOSE") { f => @@ -222,9 +287,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala deleted file mode 100644 index 933f063294..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.channel.states.c - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Transaction} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} -import org.scalatest.Outcome -import org.scalatest.funsuite.FixtureAnyFunSuiteLike - -import scala.concurrent.duration._ - -/** - * Created by PM on 05/07/2016. - */ - -class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - - val relayFees: RelayFees = RelayFees(999 msat, 1234) - - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) - - override def withFixture(test: OneArgTest): Outcome = { - val setup = init() - import setup._ - val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - within(30 seconds) { - alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) - alice2bob.expectMsgType[FundingCreated] - alice2bob.forward(bob) - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - alice2blockchain.expectMsgType[WatchFundingLost] - bob2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, router))) - } - } - - test("recv FundingLocked") { f => - import f._ - bob2alice.expectMsgType[FundingLocked] - bob2alice.forward(alice) - awaitCond(alice.stateName == NORMAL) - val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat === relayFees.feeBase) - assert(initialChannelUpdate.feeProportionalMillionths === relayFees.feeProportionalMillionths) - bob2alice.expectNoMessage(200 millis) - } - - test("recv WatchFundingSpentTriggered (remote commit)") { f => - import f._ - // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(tx) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) - awaitCond(alice.stateName == CLOSING) - } - - test("recv WatchFundingSpentTriggered (other commit)") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - awaitCond(alice.stateName == ERR_INFORMATION_LEAK) - } - - test("recv Error") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! Error(ByteVector32.Zeroes, "oops") - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) - } - - test("recv CMD_CLOSE") { f => - import f._ - val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None, None) - alice ! c - sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) - } - - test("recv CMD_FORCECLOSE") { f => - import f._ - val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! CMD_FORCECLOSE(sender.ref) - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 094a21c062..45e66ded52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -27,8 +28,9 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} -import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.Sphinx @@ -39,7 +41,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits._ @@ -72,7 +74,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) val h = randomBytes32() val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) alice ! add @@ -181,7 +183,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // Alice has a minimum set to 0 msat (which should be invalid, but may mislead Bob into relaying 0-value HTLCs which is forbidden by the spec). - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.htlcMinimum === 0.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.htlcMinimum == 0.msat) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val add = CMD_ADD_HTLC(sender.ref, 0 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add @@ -242,7 +244,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() addHtlc(758640000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend === 0.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == 0.msat) // actual test begins // at this point alice has the minimal amount to sustain a channel @@ -255,7 +257,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // but this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more val failedAdd = CMD_ADD_HTLC(sender.ref, 11000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! failedAdd - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 10000 sat, 22720 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 20000 sat, 22720 sat) sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate))) } @@ -300,8 +302,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000)) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000 msat) @@ -313,8 +315,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000)) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) val add = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -330,8 +332,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64(150000000)) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64(150000000)) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) alice ! add val error = HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat) @@ -343,8 +345,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxAcceptedHtlcs === 100) - assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.localParams.maxAcceptedHtlcs == 100) + assert(initialState.commitments.remoteParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs for (_ <- 0 until 30) { alice ! CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -361,8 +363,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs - assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 100) // Alice accepts more, but Bob will stop at 30 HTLCs + assert(initialState.commitments.localParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.remoteParams.maxAcceptedHtlcs == 100) // Alice accepts more, but Bob will stop at 30 HTLCs for (_ <- 0 until 30) { bob ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -380,11 +382,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8130.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7630.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8030.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7730.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8130.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7630.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8030.sat) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc @@ -421,7 +423,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Alice sends HTLCs to Bob that add 20 000 sat to the dust exposure. // She signs them but Bob doesn't answer yet. @@ -447,9 +449,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(alice.underlyingActor.nodeParams.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) // Alice sends HTLCs to Bob that add 21 000 sat to the dust exposure. // She signs them but Bob doesn't answer yet. @@ -471,9 +473,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 30_000.sat) - assert(alice.underlyingActor.nodeParams.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 30_000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) // Bob sends HTLCs to Alice that add 21 000 sat to the dust exposure. // He signs them but Alice doesn't answer yet. @@ -581,7 +583,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! htlc awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) // bob won't forward the add before it is cross-signed - relayerB.expectNoMessage() + bob2relayer.expectNoMessage() } test("recv UpdateAddHtlc (unexpected id)") { f => @@ -594,9 +596,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! htlc.copy(id = 3) bob ! htlc.copy(id = 42) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) + assert(new String(error.data.toArray) == UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -607,11 +609,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = 1000 msat, actual = 150 msat).getMessage) + assert(new String(error.data.toArray) == HtlcValueTooSmall(channelId(bob), minimum = 1000 msat, actual = 150 msat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -622,11 +624,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036083735L sat, reserve = 20000 sat, fees = 8960 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036083735L sat, reserve = 20000 sat, fees = 8960 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -638,11 +640,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 24760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 24760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) } test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => @@ -653,11 +655,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 11720 sat, reserve = 20000 sat, fees = 14120 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 11720 sat, reserve = 20000 sat, fees = 14120 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -669,11 +671,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -683,11 +685,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat).getMessage) + assert(new String(error.data.toArray) == HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat).getMessage) awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -701,11 +703,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) + assert(new String(error.data.toArray) == TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -753,7 +755,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // for the test to be really useful we have constraint on parameters - assert(Alice.nodeParams.dustLimit > Bob.nodeParams.dustLimit) + assert(Alice.nodeParams.channelConf.dustLimit > Bob.nodeParams.channelConf.dustLimit) // and a low feerate to avoid messing with dust exposure limits val currentFeerate = FeeratePerKw(2500 sat) alice.feeEstimator.setFeerate(FeeratesPerKw.single(currentFeerate)) @@ -761,10 +763,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(currentFeerate, alice, bob, alice2bob, bob2alice) // we're gonna exchange two htlcs in each direction, the goal is to have bob's commitment have 4 htlcs, and alice's // commitment only have 3. We will then check that alice indeed persisted 4 htlcs, and bob only 3. - val aliceMinReceive = Alice.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val aliceMinOffer = Alice.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) - val bobMinReceive = Bob.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val bobMinOffer = Bob.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) + val aliceMinReceive = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) + val aliceMinOffer = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) + val bobMinReceive = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) + val bobMinOffer = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) val a2b_1 = bobMinReceive + 10.sat // will be in alice and bob tx val a2b_2 = bobMinReceive + 20.sat // will be in alice and bob tx val b2a_1 = aliceMinReceive + 10.sat // will be in alice and bob tx @@ -793,8 +795,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here crossSign(alice, bob, alice2bob, bob2alice) // depending on who starts signing first, there will be one or two commitments because both sides have changes - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 3) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 3) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 1).size == 0) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 2).size == 2) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 3).size == 4) @@ -822,7 +824,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == htlcCount) val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs val amounts = htlcTxs.map(_.htlcTx.tx.txOut.head.amount.toLong) - assert(amounts === amounts.sorted) + assert(amounts == amounts.sorted) } test("recv CMD_SIGN (no changes)") { f => @@ -842,12 +844,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(!waitForRevocation.reSignAsap) // actual test starts here alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation)) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo == Left(waitForRevocation)) } test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { f => @@ -859,13 +861,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(!waitForRevocation.reSignAsap) // actual test starts here addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation.copy(reSignAsap = true))) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo == Left(waitForRevocation.copy(reSignAsap = true))) } test("recv CMD_SIGN (going above reserve)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -879,7 +881,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[UpdateFulfillHtlc] // we listen to channel_update events val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) // actual test starts here // when signing the fulfill, bob will have its main output go above reserve in alice's commitment tx @@ -888,13 +890,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // it should update its channel_update awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // and broadcast it - assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate === bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate == bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) } test("recv CMD_SIGN (after CMD_UPDATE_FEE)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) alice ! CMD_UPDATE_FEE(FeeratePerKw(654564 sat)) alice2bob.expectMsgType[UpdateFee] alice ! CMD_SIGN() @@ -958,14 +960,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 3) + assert(aliceCommitSig.htlcSignatures.length == 3) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 5) + assert(bobCommitSig.htlcSignatures.length == 5) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -985,14 +987,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 2) + assert(aliceCommitSig.htlcSignatures.length == 2) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 3) + assert(bobCommitSig.htlcSignatures.length == 3) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -1012,14 +1014,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 3) + assert(aliceCommitSig.htlcSignatures.length == 3) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 5) + assert(bobCommitSig.htlcSignatures.length == 5) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -1034,7 +1036,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) val updateFee = alice2bob.expectMsgType[UpdateFee] - assert(updateFee.feeratePerKw === TestConstants.feeratePerKw + FeeratePerKw(1000 sat)) + assert(updateFee.feeratePerKw == TestConstants.feeratePerKw + FeeratePerKw(1000 sat)) alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1077,8 +1079,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(new String(error.data.toArray).startsWith("cannot sign when there are no changes")) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1093,7 +1095,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1111,8 +1113,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val badCommitSig = commitSig.copy(htlcSignatures = commitSig.htlcSignatures ::: commitSig.htlcSignatures) bob ! badCommitSig val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1131,7 +1133,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! badCommitSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid htlc signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1167,15 +1169,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // at this point bob still hasn't forwarded the htlc downstream - relayerB.expectNoMessage() + bob2relayer.expectNoMessage() // actual test begins alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) // now bob will forward the htlc downstream - val forward = relayerB.expectMsgType[RelayForward] - assert(forward.add === htlc) + val forward = bob2relayer.expectMsgType[RelayForward] + assert(forward.add == htlc) } test("recv RevokeAndAck (multiple htlcs in both directions)") { f => @@ -1219,7 +1221,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap) // actual test starts here bob2alice.expectMsgType[RevokeAndAck] @@ -1242,8 +1244,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1251,9 +1253,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure)") { f => import f._ val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8030.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7730.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8030.sat) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc @@ -1271,19 +1273,19 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(bob, alice, bob2alice, alice2bob) // Alice forwards HTLCs that fit in the dust exposure. - relayerA.expectMsgAllOf( + alice2relayer.expectMsgAllOf( RelayForward(nonDust), RelayForward(almostTrimmed), RelayForward(trimmed2), ) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) // And instantly fails the others. val failedHtlcs = Seq( alice2bob.expectMsgType[UpdateFailHtlc], alice2bob.expectMsgType[UpdateFailHtlc], alice2bob.expectMsgType[UpdateFailHtlc] ) - assert(failedHtlcs.map(_.id).toSet === Set(dust1.id, dust2.id, trimmed1.id)) + assert(failedHtlcs.map(_.id).toSet == Set(dust1.id, dust2.id, trimmed1.id)) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1291,14 +1293,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Bob sends HTLCs to Alice that add 10 000 sat to the dust exposure. addHtlc(4000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) addHtlc(6000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure but doesn't sign them yet. addHtlc(6500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) @@ -1319,9 +1321,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. - relayerA.expectMsg(RelayForward(acceptedHtlc)) - relayerA.expectNoMessage(100 millis) - assert(alice2bob.expectMsgType[UpdateFailHtlc].id === rejectedHtlc.id) + alice2relayer.expectMsg(RelayForward(acceptedHtlc)) + alice2relayer.expectNoMessage(100 millis) + assert(alice2bob.expectMsgType[UpdateFailHtlc].id == rejectedHtlc.id) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1329,12 +1331,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testRevokeAndAckDustOverflowSingleCommit(f: FixtureParam): Unit = { import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Bob sends HTLCs to Alice that add 10 500 sat to the dust exposure. (1 to 10).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) crossSign(bob, alice, bob2alice, alice2bob) - (1 to 10).foreach(_ => relayerA.expectMsgType[RelayForward]) + (1 to 10).foreach(_ => alice2relayer.expectMsgType[RelayForward]) // Alice sends HTLCs to Bob that add 10 500 sat to the dust exposure but doesn't sign them yet. (1 to 10).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) @@ -1353,8 +1355,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. - (1 to 3).foreach(_ => relayerA.expectMsgType[RelayForward]) - relayerA.expectNoMessage(100 millis) + (1 to 3).foreach(_ => alice2relayer.expectMsgType[RelayForward]) + alice2relayer.expectNoMessage(100 millis) (1 to 5).foreach(_ => alice2bob.expectMsgType[UpdateFailHtlc]) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) @@ -1362,17 +1364,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ - val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.dustLimit === 5000.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ - val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.dustLimit === 1000.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 5000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } @@ -1384,8 +1384,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1405,15 +1405,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] - assert(forward.result.fail === fail) - assert(forward.htlc === htlc) + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] + assert(forward.result.fail == fail) + assert(forward.htlc == htlc) } test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => @@ -1431,15 +1431,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] - assert(forward.result.fail === fail) - assert(forward.htlc === htlc) + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] + assert(forward.result.fail == fail) + assert(forward.htlc == htlc) } def testRevokeAndAckHtlcStaticRemoteKey(f: FixtureParam): Unit = { @@ -1603,9 +1603,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)))) // alice immediately propagates the fulfill upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]] - assert(forward.result.fulfill === fulfill) - assert(forward.htlc === htlc) + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]] + assert(forward.result.fulfill == fulfill) + assert(forward.htlc == htlc) } test("recv UpdateFulfillHtlc") { @@ -1636,8 +1636,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1649,8 +1649,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1659,7 +1659,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // actual test begins @@ -1667,8 +1667,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout alice2blockchain.expectMsgType[WatchTxConfirmed] @@ -1683,7 +1683,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)) val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) - assert(fail.id === htlc.id) + assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) awaitCond(bob.stateData == initialState.copy( @@ -1807,7 +1807,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) // alice won't forward the fail before it is cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() } test("recv UpdateFailHtlc") { @@ -1841,7 +1841,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) // alice won't forward the fail before it is cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() bob ! CMD_SIGN() val sig = bob2alice.expectMsgType[CommitSig] @@ -1863,9 +1863,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.hash(htlc.onionRoutingPacket), 42) alice ! fail val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) + assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout alice2blockchain.expectMsgType[WatchTxConfirmed] @@ -1883,8 +1883,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1896,8 +1896,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1909,9 +1909,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob receives a failure with a completely invalid onion error (missing mac) bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(260)(42))) val fail = bob2alice.expectMsgType[UpdateFailHtlc] - assert(fail.id === htlc.id) + assert(fail.id == htlc.id) // We should rectify the packet length before forwarding upstream. - assert(fail.reason.length === Sphinx.FailurePacket.PacketLength) + assert(fail.reason.length == Sphinx.FailurePacket.PacketLength) } private def testCmdUpdateFee(f: FixtureParam): Unit = { @@ -1940,8 +1940,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: val sender = TestProbe() @@ -1953,7 +1953,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_UPDATE_FEE (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Alice sends an HTLC to Bob that is not included in the dust exposure at the current feerate. // She signs them but Bob doesn't answer yet. @@ -1965,8 +1965,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice sends another HTLC to Bob that is not included in the dust exposure at the current feerate. addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: val cmd = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) @@ -1984,21 +1984,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) val higherDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).max val lowerDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).min // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 3109.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 6989.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 7109.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 2989.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 3109.sat) // And the following thresholds after the feerate update // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3812.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7652.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7812.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3652.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3812.sat) // Alice send HTLCs to Bob that are not included in the dust exposure at the current feerate. // She signs them but Bob doesn't answer yet. @@ -2048,7 +2048,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val c = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) bob ! c - sender.expectMsg(RES_FAILURE(c, FundeeCannotSendUpdateFee(channelId(bob)))) + sender.expectMsg(RES_FAILURE(c, NonInitiatorCannotSendUpdateFee(channelId(bob)))) assert(initialState == bob.stateData) } @@ -2063,7 +2063,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialData.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialData.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val fee = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 0.8) bob ! fee awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) @@ -2086,8 +2086,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2100,11 +2100,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) bob ! fee val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 71620000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 71620000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2115,11 +2115,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(693364 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 1 sat, reserve = 20000 sat, fees = 780001 sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 1 sat, reserve = 20000 sat, fees = 780001 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx } test("recv UpdateFee (local/remote feerates are too different)") { f => @@ -2127,7 +2127,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.feeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.feeratePerKw) alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw * 3)) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -2137,8 +2137,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(new String(error.data.toArray).contains("local/remote feerates are too different")) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === commitTx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2147,7 +2147,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.send(bob, add) val fee = UpdateFee(initialState.channelId, TestConstants.anchorOutputsFeeratePerKw * 3) @@ -2160,7 +2160,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.send(bob, add) val fee = UpdateFee(initialState.channelId, FeeratePerKw(FeeratePerByte(2 sat))) @@ -2177,11 +2177,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bobCommitments.localCommit.spec.commitTxFeerate == expectedFeeratePerKw) bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") + assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2195,23 +2195,23 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-closes: bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(20000 sat))) bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } test("recv UpdateFee (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 30_000.sat) + assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 30_000.sat) // Bob sends HTLCs to Alice that are not included in the dust exposure at the current feerate. // He signs them but Alice doesn't answer yet. @@ -2224,16 +2224,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob sends another HTLC to Alice that is not included in the dust exposure at the current feerate. addHtlc(14000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-close: val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(20000 sat))) bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } @@ -2247,21 +2247,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) val higherDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).max val lowerDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).min // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 3109.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 6989.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 7109.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 2989.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 3109.sat) // And the following thresholds after the feerate update // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3812.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7652.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7812.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3652.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3812.sat) // Bob send HTLCs to Alice that are not included in the dust exposure at the current feerate. // He signs them but Alice doesn't answer yet. @@ -2279,8 +2279,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! UpdateFee(channelId(bob), updatedFeerate) val error = bob2alice.expectMsgType[Error] // NB: we don't need to distinguish local and remote, the error message is exactly the same. - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 37000000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 37000000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } @@ -2305,7 +2305,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) assert(localUpdate.channelUpdate.cltvExpiryDelta == newCltvExpiryDelta) - relayerA.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(1 seconds) } def testCmdClose(f: FixtureParam, script_opt: Option[ByteVector]): Unit = { @@ -2315,7 +2315,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_CLOSE(sender.ref, script_opt, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] - script_opt.foreach(script => assert(script === shutdown.scriptPubKey)) + script_opt.foreach(script => assert(script == shutdown.scriptPubKey)) awaitCond(alice.stateName == NORMAL) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } @@ -2436,7 +2436,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) } - test("recv CMD_CLOSE (with a script that does not match our upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (with a script that does not match our upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() val shutdownScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) @@ -2444,7 +2444,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } - test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() val shutdownScript = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey @@ -2456,7 +2456,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } - test("recv CMD_CLOSE (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() alice ! CMD_CLOSE(sender.ref, None, None) @@ -2478,7 +2478,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } @@ -2490,7 +2490,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[ClosingSigned] awaitCond(alice.stateName == NEGOTIATING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) } test("recv Shutdown (no pending htlcs)") { f => @@ -2523,7 +2523,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == SHUTDOWN) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) } test("recv Shutdown (with unacked received htlcs)") { f => @@ -2609,7 +2609,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("recv Shutdown (with a script that does not match the upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv Shutdown (with a script that does not match the upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ bob ! Shutdown(ByteVector32.Zeroes, Script.write(Script.pay2wpkh(randomKey().publicKey))) @@ -2719,11 +2719,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) } test("recv CurrentBlockCount (fulfilled signed htlc ignored by upstream peer)") { f => @@ -2732,7 +2732,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill @@ -2746,16 +2746,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2765,7 +2765,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill but doesn't sign @@ -2779,16 +2779,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = false) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectNoMessage(500 millis) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2798,7 +2798,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill @@ -2816,16 +2816,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2840,7 +2840,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) alice2bob.expectMsgType[CommitSig] @@ -2859,7 +2859,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectNoMessage(500 millis) } @@ -2898,14 +2898,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw / 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw / 2) // The network fees spike, but Bob doesn't close the channel because we're using anchor outputs. bob.feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) val event = CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) bob ! event bob2alice.expectNoMessage(250 millis) - assert(bob.stateName === NORMAL) + assert(bob.stateName == NORMAL) } test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, without HTLCs)") { f => @@ -2965,14 +2965,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }).sum // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed === 814880.sat) + assert(amountClaimed == 814880.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlcb1.id -> htlcb1.cltvExpiry.toLong)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlcb1.id -> htlcb1.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -2982,7 +2982,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 4) + assert(rcp.claimHtlcTxs.size == 4) assert(getClaimHtlcSuccessTxs(rcp).length == 1) assert(getClaimHtlcTimeoutTxs(rcp).length == 2) @@ -3005,7 +3005,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } @@ -3056,13 +3056,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }).sum // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed === 822310.sat) + assert(amountClaimed == 822310.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) // claim-main + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // claim-main alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -3071,8 +3071,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 2) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 2) } test("recv WatchFundingSpentTriggered (their *next* commit w/ pending unsigned htlcs)") { f => @@ -3094,7 +3094,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc2) } @@ -3131,11 +3131,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTxs = for (_ <- 0 until 4) yield alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(1 second) @@ -3144,12 +3144,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 760 000 and 200 000 - assert(mainTx.txOut.head.amount === 741500.sat) - assert(mainPenaltyTx.txOut.head.amount === 195160.sat) - assert(htlcPenaltyTxs(0).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(1).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(2).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(3).txOut.head.amount === 4540.sat) + assert(mainTx.txOut.head.amount == 741500.sat) + assert(mainPenaltyTx.txOut.head.amount == 195160.sat) + assert(htlcPenaltyTxs(0).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(1).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(2).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(3).txOut.head.amount == 4540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -3196,9 +3196,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTxs = for (_ <- 0 until 2) yield alice2blockchain.expectMsgType[PublishFinalTx].tx // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(1 second) @@ -3227,7 +3227,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc3) } @@ -3256,15 +3256,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get assert(localCommitPublished.commitTx.txid == aliceCommitTx.txid) - assert(localCommitPublished.htlcTxs.size === 4) - assert(getHtlcSuccessTxs(localCommitPublished).length === 1) - assert(getHtlcTimeoutTxs(localCommitPublished).length === 2) + assert(localCommitPublished.htlcTxs.size == 4) + assert(getHtlcSuccessTxs(localCommitPublished).length == 1) + assert(getHtlcTimeoutTxs(localCommitPublished).length == 2) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage @@ -3279,8 +3279,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the main delayed output and htlc txs spend the commitment transaction Seq(claimMain, htlcTx1, htlcTx2, htlcTx3).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) // main-delayed + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // main-delayed alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -3290,17 +3290,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 3rd-stage txs are published when htlc txs confirm Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTimeoutTx => alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) alice2blockchain.expectNoMessage(1 second) } - test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = { import f._ val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) @@ -3314,12 +3314,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchors and 4 pending htlcs awaitCond(alice.stateName == CLOSING) val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(localAnchor.txInfo.confirmBefore.toLong === htlca1.cltvExpiry.toLong) // the target is set to match the first htlc that expires + assert(localAnchor.txInfo.confirmBefore.toLong == htlca1.cltvExpiry.toLong) // the target is set to match the first htlc that expires val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val htlcConfirmationTargets = Seq( @@ -3327,10 +3327,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 2 alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 3 ).map(p => p.txInfo.asInstanceOf[HtlcTx].htlcId -> p.txInfo.confirmBefore.toLong).toMap - assert(htlcConfirmationTargets === Map(htlcb1.id -> htlcb1.cltvExpiry.toLong, htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(htlcConfirmationTargets == Map(htlcb1.id -> htlcb1.cltvExpiry.toLong, htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) val watchedOutputs = Seq( alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 @@ -3339,30 +3339,54 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchOutputSpent], // local anchor ).map(w => OutPoint(w.txId.reverse, w.outputIndex)).toSet val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(watchedOutputs === localCommitPublished.htlcTxs.keySet + localAnchor.txInfo.input.outPoint) + assert(watchedOutputs == localCommitPublished.htlcTxs.keySet + localAnchor.txInfo.input.outPoint) alice2blockchain.expectNoMessage(1 second) } - test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testErrorAnchorOutputsWithHtlcs(f) + } + + test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + // We should ignore the disable flag since there are htlcs in the commitment (funds at risk). + testErrorAnchorOutputsWithHtlcs(f) + } + + def testErrorAnchorOutputsWithoutHtlcs(f: FixtureParam, commitFeeBumpDisabled: Boolean): Unit = { import f._ // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors awaitCond(alice.stateName == CLOSING) val currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight val blockTargets = alice.underlyingActor.nodeParams.onChainFeeConf.feeTargets - val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - // When there are no pending HTLCs, there is no rush to get the commit tx confirmed - assert(localAnchor.txInfo.confirmBefore === currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) - alice2blockchain.expectNoMessage(1 second) + if (commitFeeBumpDisabled) { + val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + alice2blockchain.expectNoMessage(1 second) + } else { + val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] + // When there are no pending HTLCs, there is no rush to get the commit tx confirmed + assert(localAnchor.txInfo.confirmBefore === currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) + val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) + alice2blockchain.expectNoMessage(1 second) + } + } + + test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) + } + + test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = true) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -3374,7 +3398,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! Error(ByteVector32.Zeroes, "oops") - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === bobCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) assert(bobCommitTx.txOut.size == 1) // only one main output alice2blockchain.expectNoMessage(1 second) @@ -3394,82 +3418,158 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! Error(ByteVector32.Zeroes, "oops") - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } - test("recv WatchFundingDeeplyBuriedTriggered", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } - test("recv WatchFundingDeeplyBuriedTriggered (short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) + // in zero-conf channel we don't have a real short channel id when going to NORMAL state + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + // funding tx coordinates (unknown before) + val (blockHeight, txIndex) = (BlockHeight(400000), 42) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + val realShortChannelId = RealShortChannelId(blockHeight, txIndex, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (public channel, short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + assert(annSigs.shortChannelId == newRealShortChannelId) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // we create a new listener that registers after alice has published the funding tx + val listener = TestProbe() + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + // zero-conf channel: the funding tx isn't confirmed + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42000), 42, null) + val realShortChannelId = RealShortChannelId(BlockHeight(42000), 42, 0) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) + // this is the first time we know the funding tx has been confirmed + listener.expectMsgType[TransactionConfirmed] } test("recv WatchFundingDeeplyBuriedTriggered (private channel, short channel id changed)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // LocalChannelUpdate should not be published - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv AnnouncementSignatures", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here - bob2alice.forward(alice) - awaitCond({ + bob2alice.forward(alice, annSigsB) + awaitAssert { val normal = alice.stateData.asInstanceOf[DATA_NORMAL] - normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId - }) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn)) + assert(normal.shortIds.real == RealScidStatus.Final(annSigsA.shortChannelId) && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId) + } + // we use the real scid instead of remote alias as soon as the channel is announced + val lcu = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(lcu.channelUpdate.shortChannelId == realShortChannelId) + assert(lcu.channelAnnouncement_opt.contains(channelAnn)) + // we don't send directly the channel_update to our peer, public announcements are handled by the router + alice2bob.expectNoMessage(100 millis) } test("recv AnnouncementSignatures (re-send)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.contains(channelAnn)) // actual test starts here // simulate bob re-sending its sigs @@ -3498,14 +3598,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - bob2alice.expectMsgType[AnnouncementSignatures] + val realScid = bob2alice.expectMsgType[AnnouncementSignatures].shortChannelId bob2alice.forward(alice) val update1 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.shortChannelId == realScid) // actual test starts here Thread.sleep(1100) alice ! BroadcastChannelUpdate(PeriodicRefresh) val update2 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update2.channelUpdate.shortChannelId == realScid) assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) } @@ -3525,9 +3627,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // actual test starts here alice ! INPUT_DISCONNECTED @@ -3538,10 +3638,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED (with pending unsigned htlcs)") { f => import f._ + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) val sender = TestProbe() - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) val (_, htlc1) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val (_, htlc2) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) @@ -3552,10 +3650,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! INPUT_DISCONNECTED - val addSettled1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + val addSettled1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] assert(addSettled1.htlc == htlc1) assert(addSettled1.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) - val addSettled2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + val addSettled2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] assert(addSettled2.htlc == htlc2) assert(addSettled2.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) assert(!channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) @@ -3599,8 +3697,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! INPUT_DISCONNECTED - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc1.paymentHash) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc2.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash == htlc1.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash == htlc2.paymentHash) val update2a = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(update1a.channelUpdate.timestamp < update2a.channelUpdate.timestamp) assert(!update2a.channelUpdate.channelFlags.isEnabled) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 747c7f12b6..6a66d8a789 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -18,18 +18,21 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, TestUtils, TimestampMilli, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -43,24 +46,32 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with type FixtureParam = SetupFixture + /** If set, do not close channel in case of a fee mismatch when disconnected */ + val DisableOfflineMismatch = "disable_offline_mismatch" + /** If set, channel_update will be ignored */ + val IgnoreChannelUpdates = "ignore_channel_updates" + override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val aliceParams = Alice.nodeParams + .modify(_.onChainFeeConf.closeOnOfflineMismatch).setToIf(test.tags.contains(DisableOfflineMismatch))(false) + val setup = init(nodeParamsA = aliceParams) import setup._ within(30 seconds) { reachNormal(setup) + if (test.tags.contains(IgnoreChannelUpdates)) { + setup.alice2bob.ignoreMsg({ case _: ChannelUpdate => true }) + setup.bob2alice.ignoreMsg({ case _: ChannelUpdate => true }) + } awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) withFixture(test.toNoArgTest(setup)) } } - val aliceInit = Init(TestConstants.Alice.nodeParams.features) - val bobInit = Init(TestConstants.Bob.nodeParams.features) + val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) + val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) - test("re-send lost htlc and signature after first commitment") { f => + test("re-send lost htlc and signature after first commitment", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -83,9 +94,9 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, reestablishA) bob2alice.forward(alice, reestablishB) - // both nodes will send the funding_locked message because all updates have been cancelled - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // both nodes will send the channel_ready message because all updates have been cancelled + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] // alice will re-send the update and the sig alice2bob.expectMsg(htlc) @@ -113,7 +124,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost revocation") { f => + test("re-send lost revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -157,7 +168,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost signature after revocation") { f => + test("re-send lost signature after revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -230,7 +241,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("resume htlc settlement") { f => + test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => import f._ // Successfully send a first payment. @@ -254,11 +265,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with reconnect(alice, bob, alice2bob, bob2alice) val reestablishA = alice2bob.expectMsgType[ChannelReestablish] - assert(reestablishA.nextLocalCommitmentNumber === 4) - assert(reestablishA.nextRemoteRevocationNumber === 3) + assert(reestablishA.nextLocalCommitmentNumber == 4) + assert(reestablishA.nextRemoteRevocationNumber == 3) val reestablishB = bob2alice.expectMsgType[ChannelReestablish] - assert(reestablishB.nextLocalCommitmentNumber === 5) - assert(reestablishB.nextRemoteRevocationNumber === 3) + assert(reestablishB.nextLocalCommitmentNumber == 5) + assert(reestablishB.nextRemoteRevocationNumber == 3) bob2alice.forward(alice, reestablishB) // alice does not re-send messages bob already received @@ -273,11 +284,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 4) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 4) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) } - test("reconnect with an outdated commitment") { f => + test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) @@ -311,7 +322,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, reestablishB) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(error === Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) + assert(error == Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) alice2bob.forward(bob) // alice now waits for bob to publish its commitment @@ -327,7 +338,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("reconnect with an outdated commitment (but counterparty can't tell)") { f => + test("reconnect with an outdated commitment (but counterparty can't tell)", Tag(IgnoreChannelUpdates)) { f => import f._ // we start by storing the current state @@ -368,7 +379,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, reestablishB) // alice asks bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(error === Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) + assert(error == Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) alice2bob.forward(bob) // alice now waits for bob to publish its commitment @@ -382,7 +393,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("counterparty lies about having a more recent commitment") { f => + test("counterparty lies about having a more recent commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -398,13 +409,13 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice then finds out bob is lying bob2alice.send(alice, invalidReestablish) val error = alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) val claimMainOutput = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainOutput, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(error === Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) + assert(error == Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) } - test("change relay fee while offline") { f => + test("change relay fee while offline", Tag(IgnoreChannelUpdates)) { f => import f._ val sender = TestProbe() @@ -432,8 +443,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then alice reaches NORMAL state, and after a delay she broadcasts the channel_update val channelUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate](20 seconds).channelUpdate - assert(channelUpdate.feeBaseMsat === 4200.msat) - assert(channelUpdate.feeProportionalMillionths === 123456) + assert(channelUpdate.feeBaseMsat == 4200.msat) + assert(channelUpdate.feeProportionalMillionths == 123456) assert(channelUpdate.channelFlags.isEnabled) // no more messages @@ -459,7 +470,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(!update.channelUpdate.channelFlags.isEnabled) } - test("replay pending commands when going back to NORMAL") { f => + test("replay pending commands when going back to NORMAL", Tag(IgnoreChannelUpdates)) { f => import f._ val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -525,7 +536,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -536,22 +547,22 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending fulfill on that HTLC but not relayed. // When it is close to expiring upstream, we should close the channel. bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed bob2blockchain.expectMsgType[WatchOutputSpent] // htlc - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed bob2blockchain.expectMsgType[WatchOutputSpent] // htlc bob2blockchain.expectNoMessage(500 millis) @@ -567,7 +578,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. bob ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) bob2blockchain.expectNoMessage(250 millis) alice2blockchain.expectNoMessage(250 millis) @@ -605,13 +616,13 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice is funder alice ! CurrentFeerates(networkFeerate) if (shouldClose) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) } else { alice2blockchain.expectNoMessage() } } - test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f => + test("handle feerate changes while offline (don't close on mismatch)", Tag(DisableOfflineMismatch)) { f => import f._ // we only close channels on feerate mismatch if there are HTLCs at risk in the commitment @@ -658,7 +669,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) - alice2bob.expectMsgType[FundingLocked] // since the channel's commitment hasn't been updated, we re-send funding_locked + alice2bob.expectMsgType[ChannelReady] // since the channel's commitment hasn't been updated, we re-send channel_ready if (shouldUpdateFee) { alice2bob.expectMsg(UpdateFee(channelId(alice), networkFeeratePerKw)) } else { @@ -667,11 +678,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("handle feerate changes while offline (update at reconnection)") { f => + test("handle feerate changes while offline (update at reconnection)", Tag(IgnoreChannelUpdates)) { f => testUpdateFeeOnReconnect(f, shouldUpdateFee = true) } - test("handle feerate changes while offline (shutdown sent, don't update at reconnection)") { f => + test("handle feerate changes while offline (shutdown sent, don't update at reconnection)", Tag(IgnoreChannelUpdates)) { f => import f._ // alice initiates a shutdown @@ -714,26 +725,33 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob is fundee bob ! CurrentFeerates(networkFeerate) if (shouldClose) { - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === bobCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) } else { bob2blockchain.expectNoMessage() } } - test("re-send channel_update at reconnection for private channels") { f => + test("re-send channel_update at reconnection for unannounced channels") { f => import f._ // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel isn't deeply buried: channel_update isn't sent again - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // alice and bob resend their channel_ready because there hasn't been payments on the channel + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] + + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() @@ -743,24 +761,31 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel still isn't deeply buried: channel_update isn't sent again + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() - // funding tx gets 6 confirmations, channel is private so there is no announcement sigs + // funding tx gets 6 confirmations alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - alice2bob.expectMsgType[ChannelUpdate] - bob2alice.expectMsgType[ChannelUpdate] + // channel is private so there is no announcement sigs + // we use aliases so there is no need to resend a channel_update // we get disconnected again disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] @@ -785,12 +810,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) - val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments + val aliceCommitments = alice.stateData.asInstanceOf[PersistentChannelData].commitments val aliceCurrentPerCommitmentPoint = TestConstants.Alice.channelKeyManager.commitmentPoint( TestConstants.Alice.channelKeyManager.keyPath(aliceCommitments.localParams, aliceCommitments.channelConfig), aliceCommitments.localCommit.index) - val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments + val bobCommitments = bob.stateData.asInstanceOf[PersistentChannelData].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 134357387c..0668a17a0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.channel.states.f import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} @@ -59,7 +60,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h1 = Crypto.sha256(r1) val amount1 = 300000000 msat val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None)).get._1.copy(commit = false) + val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, Some(randomBytes32()), None)).get._1.copy(commit = false) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -69,7 +70,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h2 = Crypto.sha256(r2) val amount2 = 200000000 msat val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None)).get._1.copy(commit = false) + val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, Some(randomBytes32()), None)).get._1.copy(commit = false) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -86,8 +87,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - relayerB.expectMsgType[RelayForward] - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] // alice initiates a closing alice ! CMD_CLOSE(sender.ref, None, None) alice2bob.expectMsgType[Shutdown] @@ -184,7 +185,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! fulfill alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -197,7 +198,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -288,7 +289,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -309,9 +310,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, 1, Crypto.sha256(ByteVector.empty), 42) alice ! fail val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) + assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -353,12 +354,12 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.expectMsgType[CommitSig] awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(waitForRevocation.reSignAsap == false) // actual test starts here bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) sender.expectNoMessage(300 millis) - assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo === Left(waitForRevocation)) + assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo == Left(waitForRevocation)) } test("recv CommitSig") { f => @@ -379,7 +380,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -390,7 +391,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -446,7 +447,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[PublishTx] // htlc success bob2blockchain.expectMsgType[WatchTxConfirmed] @@ -459,7 +460,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -479,14 +480,14 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] - assert(forward.result.fail === fail) + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] + assert(forward.result.fail == fail) } test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => @@ -502,14 +503,14 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] - assert(forward.result.fail === fail) + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] + assert(forward.result.fail == fail) } test("recv CMD_UPDATE_FEE") { f => @@ -529,7 +530,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] bob ! CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) - sender.expectMsgType[RES_FAILURE[CMD_UPDATE_FEE, FundeeCannotSendUpdateFee]] + sender.expectMsgType[RES_FAILURE[CMD_UPDATE_FEE, NonInitiatorCannotSendUpdateFee]] assert(initialState == bob.stateData) } @@ -547,7 +548,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -562,9 +563,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) bob ! fee val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -574,9 +575,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(65000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") + assert(new String(error.data.toArray) == "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -586,9 +587,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") + assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -600,7 +601,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth, cltvExpiryDelta_opt = None) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] - relayerA.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(1 seconds) } test("recv CurrentBlockCount (no htlc timed out)") { f => @@ -615,11 +616,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) } test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => @@ -633,7 +634,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) } @@ -648,7 +649,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectNoMessage(500 millis) } @@ -690,10 +691,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) val amountClaimed = htlcAmountClaimed + claimMain.txOut.head.amount - assert(amountClaimed === 774040.sat) + assert(amountClaimed == 774040.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) @@ -701,9 +702,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 2) - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 2) + assert(rcp.claimHtlcTxs.size == 2) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 2) } test("recv WatchFundingSpentTriggered (their next commit)") { f => @@ -740,19 +741,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit claimTx.txOut.head.amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) - assert(amountClaimed === 481210.sat) + assert(amountClaimed == 481210.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimTxs(0).txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 1) - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 1) + assert(rcp.claimHtlcTxs.size == 1) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 1) } test("recv WatchFundingSpentTriggered (revoked tx)") { f => @@ -775,8 +776,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc1-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc2-penalty @@ -788,10 +789,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut.head.amount === 284940.sat) - assert(mainPenaltyTx.txOut.head.amount === 195160.sat) - assert(htlc1PenaltyTx.txOut.head.amount === 194540.sat) - assert(htlc2PenaltyTx.txOut.head.amount === 294540.sat) + assert(mainTx.txOut.head.amount == 284940.sat) + assert(mainPenaltyTx.txOut.head.amount == 195160.sat) + assert(htlc1PenaltyTx.txOut.head.amount == 194540.sat) + assert(htlc2PenaltyTx.txOut.head.amount == 294540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -800,13 +801,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)") { f => import f._ val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialCommitTx.txOut.size === 4) // two main outputs + 2 htlc + assert(initialCommitTx.txOut.size == 4) // two main outputs + 2 htlc // bob fulfills one of the pending htlc (commitment update while in shutdown state) fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(revokedTx.txOut.size === 3) // two main outputs + 1 htlc + assert(revokedTx.txOut.size == 3) // two main outputs + 1 htlc // bob fulfills the second pending htlc (and revokes the previous commitment) fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) @@ -820,8 +821,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-penalty alice2blockchain.expectNoMessage(1 second) @@ -831,9 +832,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut(0).amount === 286660.sat) - assert(mainPenaltyTx.txOut(0).amount === 495160.sat) - assert(htlcPenaltyTx.txOut(0).amount === 194540.sat) + assert(mainTx.txOut(0).amount == 286660.sat) + assert(mainPenaltyTx.txOut(0).amount == 495160.sat) + assert(htlcPenaltyTx.txOut(0).amount == 194540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -852,7 +853,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val closingFeerates1 = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(2500 sat)) alice ! CMD_CLOSE(sender.ref, None, Some(closingFeerates1)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] - assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates1)) + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates == Some(closingFeerates1)) val closingScript = alice.stateData.asInstanceOf[DATA_SHUTDOWN].localShutdown.scriptPubKey val closingFeerates2 = ClosingFeerates(FeeratePerKw(600 sat), FeeratePerKw(300 sat), FeeratePerKw(2500 sat)) @@ -860,7 +861,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] alice ! CMD_CLOSE(sender.ref, Some(closingScript), Some(closingFeerates2)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] - assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates2)) + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates == Some(closingFeerates2)) } test("recv CMD_FORCECLOSE") { f => @@ -872,19 +873,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size === 2) + assert(lcp.htlcTxs.size == 2) assert(lcp.claimHtlcDelayedTxs.isEmpty) // 3rd-stage txs will be published once htlc txs confirm val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc1 = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc2 = alice2blockchain.expectMsgType[PublishFinalTx].tx Seq(claimMain, htlc1, htlc2).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) @@ -892,11 +893,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // 3rd-stage txs are published when htlc txs confirm Seq(htlc1, htlc2).foreach(htlcTimeoutTx => { alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) }) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 2) alice2blockchain.expectNoMessage(1 second) @@ -906,7 +907,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -918,8 +919,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val claimTxs = for (_ <- 0 until 3) yield alice2blockchain.expectMsgType[PublishFinalTx].tx // the main delayed output and htlc txs spend the commitment transaction claimTxs.foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimTxs(0).txid) // main-delayed + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) // main-delayed alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 30900556be..63893abd7f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -17,11 +17,12 @@ package fr.acinq.eclair.channel.states.g import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions @@ -61,11 +62,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val bobShutdown = bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice, bobShutdown) awaitCond(alice.stateName == NEGOTIATING) - if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (alice.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } awaitCond(bob.stateName == NEGOTIATING) - if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (bob.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } } @@ -80,11 +81,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceShutdown = alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob, aliceShutdown) awaitCond(alice.stateName == NEGOTIATING) - if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (alice.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } awaitCond(bob.stateName == NEGOTIATING) - if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (bob.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } } @@ -122,19 +123,19 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) assert(aliceCloseSig1.feeRange_opt.get.min < aliceCloseSig1.feeSatoshis) assert(aliceCloseSig1.feeSatoshis < aliceCloseSig1.feeRange_opt.get.max) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length === 1) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length == 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) if (alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { // check that the closing tx uses Alice and Bob's default closing scripts val closingTx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.head.unsignedTx.tx val expectedLocalScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey val expectedRemoteScript = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey - assert(closingTx.txOut.map(_.publicKeyScript).toSet === Set(expectedLocalScript, expectedRemoteScript)) + assert(closingTx.txOut.map(_.publicKeyScript).toSet == Set(expectedLocalScript, expectedRemoteScript)) } alice2bob.forward(bob) // bob answers with a counter proposition in alice's fee range @@ -147,19 +148,19 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.forward(alice) // alice accepts this proposition val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig2.feeSatoshis === bobCloseSig1.feeSatoshis) + assert(aliceCloseSig2.feeSatoshis == bobCloseSig1.feeSatoshis) alice2bob.forward(bob) assert(alice.stateName == CLOSING) assert(bob.stateName == CLOSING) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(mutualCloseTx.txOut.length == 2) // NB: in the anchor outputs case, anchors are removed from the closing tx assert(aliceCloseSig2.feeSatoshis > Transactions.weight2fee(TestConstants.anchorOutputsFeeratePerKw, mutualCloseTx.weight())) // NB: closing fee is allowed to be higher than commit tx fee when using anchor outputs - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) - assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTx)) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTx)) } test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { f => @@ -174,7 +175,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedDifferentFees(f) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedDifferentFees(f) } @@ -186,7 +187,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob) val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeSatoshis) + assert(bobCloseSig.feeSatoshis == aliceCloseSig.feeSatoshis) } test("recv ClosingSigned (theirMaxCloseFee < ourCloseFee)") { f => @@ -197,7 +198,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob) val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeRange_opt.get.max) + assert(bobCloseSig.feeSatoshis == aliceCloseSig.feeRange_opt.get.max) } private def testClosingSignedSameFees(f: FixtureParam, bobInitiates: Boolean = false): Unit = { @@ -215,16 +216,16 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5 000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5 000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) alice2bob.forward(bob) // bob agrees with that proposal val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig1.feeSatoshis === aliceCloseSig1.feeSatoshis) + assert(bobCloseSig1.feeSatoshis == aliceCloseSig1.feeSatoshis) val mutualCloseTx = bob2blockchain.expectMsgType[PublishFinalTx].tx - assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + assert(mutualCloseTx.txOut.length == 2) // NB: in the anchor outputs case, anchors are removed from the closing tx bob2alice.forward(alice) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) assert(alice.stateName == CLOSING) assert(bob.stateName == CLOSING) } @@ -241,7 +242,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedSameFees(f) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedSameFees(f) } @@ -252,20 +253,20 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice initiates the negotiation with a very low feerate val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === 1685.sat) - assert(aliceCloseSig.feeRange_opt === Some(FeeRange(1348 sat, 2022 sat))) + assert(aliceCloseSig.feeSatoshis == 1685.sat) + assert(aliceCloseSig.feeRange_opt == Some(FeeRange(1348 sat, 2022 sat))) alice2bob.forward(bob) // bob chooses alice's highest fee val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === 2022.sat) + assert(bobCloseSig.feeSatoshis == 2022.sat) bob2alice.forward(alice) // alice accepts this proposition - assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis === 2022.sat) + assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis == 2022.sat) alice2bob.forward(bob) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("override on-chain fee estimator (fundee)") { f => @@ -275,16 +276,16 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bobClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice is funder, so bob's override will simply be ignored val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === 6740.sat) // matches a feerate of 10000 sat/kw + assert(aliceCloseSig.feeSatoshis == 6740.sat) // matches a feerate of 10000 sat/kw alice2bob.forward(bob) // bob directly agrees because their fee estimator matches val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === bobCloseSig.feeSatoshis) + assert(aliceCloseSig.feeSatoshis == bobCloseSig.feeSatoshis) bob2alice.forward(alice) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("recv ClosingSigned (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -295,9 +296,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - assert(aliceCloseFee === bobCloseFee) + assert(aliceCloseFee == bobCloseFee) bob2blockchain.expectMsgType[PublishTx] - awaitCond(bob.stateName === CLOSING) + awaitCond(bob.stateName == CLOSING) } private def makeLegacyClosingSigned(f: FixtureParam, closingFee: Satoshi): (ClosingSigned, ClosingSigned) = { @@ -308,8 +309,8 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val bobState = bob.stateData.asInstanceOf[DATA_NEGOTIATING] val bobKeyManager = bob.underlyingActor.nodeParams.channelKeyManager val bobScript = bobState.localShutdown.scriptPubKey - val (_, aliceClosingSigned) = Closing.makeClosingTx(aliceKeyManager, aliceState.commitments, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) - val (_, bobClosingSigned) = Closing.makeClosingTx(bobKeyManager, bobState.commitments, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, aliceClosingSigned) = Closing.MutualClose.makeClosingTx(aliceKeyManager, aliceState.commitments, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, bobClosingSigned) = Closing.MutualClose.makeClosingTx(bobKeyManager, bobState.commitments, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) (aliceClosingSigned.copy(tlvStream = TlvStream.empty), bobClosingSigned.copy(tlvStream = TlvStream.empty)) } @@ -319,9 +320,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f) val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] val Some(FeeRange(_, maxFee)) = aliceClosing1.feeRange_opt - assert(aliceClosing1.feeSatoshis === 674.sat) - assert(maxFee === 1348.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(aliceClosing1.feeSatoshis == 674.sat) + assert(maxFee == 1348.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) // bob makes a proposal outside our fee range val (_, bobClosing1) = makeLegacyClosingSigned(f, 2500 sat) @@ -329,27 +330,27 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing1.feeSatoshis < aliceClosing2.feeSatoshis) assert(aliceClosing2.feeSatoshis < 1600.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 2) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing2) = makeLegacyClosingSigned(f, 2000 sat) bob2alice.send(alice, bobClosing2) val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing2.feeSatoshis < aliceClosing3.feeSatoshis) assert(aliceClosing3.feeSatoshis < 1800.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 3) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing3) = makeLegacyClosingSigned(f, 1800 sat) bob2alice.send(alice, bobClosing3) val aliceClosing4 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing3.feeSatoshis < aliceClosing4.feeSatoshis) assert(aliceClosing4.feeSatoshis < 1800.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 4) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing4) = makeLegacyClosingSigned(f, aliceClosing4.feeSatoshis) bob2alice.send(alice, bobClosing4) - awaitCond(alice.stateName === CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (other side ignores our fee range, fundee)") { f => @@ -361,34 +362,34 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.send(bob, aliceClosing1) val bobClosing1 = bob2alice.expectMsgType[ClosingSigned] assert(3000.sat < bobClosing1.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing2, _) = makeLegacyClosingSigned(f, 750 sat) alice2bob.send(bob, aliceClosing2) val bobClosing2 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing2.feeSatoshis < bobClosing1.feeSatoshis) assert(2000.sat < bobClosing2.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 2) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing3, _) = makeLegacyClosingSigned(f, 1000 sat) alice2bob.send(bob, aliceClosing3) val bobClosing3 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing3.feeSatoshis < bobClosing2.feeSatoshis) assert(1500.sat < bobClosing3.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 3) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing4, _) = makeLegacyClosingSigned(f, 1300 sat) alice2bob.send(bob, aliceClosing4) val bobClosing4 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing4.feeSatoshis < bobClosing3.feeSatoshis) assert(1300.sat < bobClosing4.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 4) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing5, _) = makeLegacyClosingSigned(f, bobClosing4.feeSatoshis) alice2bob.send(bob, aliceClosing5) - awaitCond(bob.stateName === CLOSING) - assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(bob.stateName == CLOSING) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (other side ignores our fee range, max iterations reached)") { f => @@ -402,9 +403,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val (_, bobClosing) = makeLegacyClosingSigned(f, bobNextFee) bob2alice.send(alice, bobClosing) } - awaitCond(alice.stateName === CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (fee too low, fundee)") { f => @@ -428,7 +429,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.forward(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000 sat")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -441,7 +442,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -453,12 +454,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.forward(bob, aliceCloseSig) // at this point alice and bob agree on closing fees, but alice has not yet received the final signature whereas bob has // bob publishes the mutual close and alice is notified that the funding tx has been spent - assert(alice.stateName === NEGOTIATING) + assert(alice.stateName == NEGOTIATING) val mutualCloseTx = bob2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) alice ! WatchFundingSpentTriggered(mutualCloseTx) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == CLOSING) } @@ -481,10 +482,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(firstMutualCloseTx.tx.txid !== latestMutualCloseTx.tx.txid) // at this point bob will receive a new signature, but he decides instead to publish the first mutual close alice ! WatchFundingSpentTriggered(firstMutualCloseTx.tx) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === firstMutualCloseTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === firstMutualCloseTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == firstMutualCloseTx.tx.txid) alice2blockchain.expectNoMessage(100 millis) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) } test("recv WatchFundingSpentTriggered (self mutual close)") { f => @@ -497,11 +498,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.expectMsgType[ClosingSigned] // at this point bob has received a mutual close signature from alice, but doesn't yet agree on the fee // bob's mutual close is published from the outside of the actor - assert(bob.stateName === NEGOTIATING) + assert(bob.stateName == NEGOTIATING) val mutualCloseTx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.get.tx bob ! WatchFundingSpentTriggered(mutualCloseTx) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) bob2blockchain.expectNoMessage(100 millis) assert(bob.stateName == CLOSING) } @@ -541,9 +542,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob, aliceClosing3) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("recv Error") { f => @@ -553,9 +554,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 9a50a368c2..556b453386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -17,12 +17,14 @@ package fr.acinq.eclair.channel.states.h import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} -import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ @@ -43,7 +45,7 @@ import scala.concurrent.duration._ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) override def withFixture(test: OneArgTest): Outcome = { val setup = init() @@ -64,10 +66,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with if (unconfirmedFundingTx) { within(30 seconds) { val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] @@ -87,23 +90,27 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchFundingConfirmed] awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, eventListener, Nil))) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, alice2relayer, bob2relayer, channelUpdateListener, eventListener, Nil))) } } else { within(30 seconds) { reachNormal(setup, test.tags) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) // alice forwards the fulfill upstream - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] crossSign(bob, alice, bob2alice, alice2bob) // bob confirms that it has forwarded the fulfill to alice awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(htlc.channelId).isEmpty) @@ -113,7 +120,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, eventListener, bobCommitTxs))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, alice2relayer, bob2relayer, channelUpdateListener, eventListener, bobCommitTxs))) } } } @@ -180,7 +187,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // test starts here alice ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) alice2bob.expectNoMessage(200 millis) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === fundingTx) // we republish the funding tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == fundingTx) // we republish the funding tx assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING } @@ -271,7 +278,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testMutualCloseBeforeConverge(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(2500 sat)).copy(mempoolMinFee = FeeratePerKw(250 sat), blocks_1008 = FeeratePerKw(250 sat))) // alice initiates a closing with a low fee alice ! CMD_CLOSE(sender.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(1000 sat)))) @@ -288,13 +295,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // let's make bob publish this closing tx bob ! Error(ByteVector32.Zeroes, "") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx.tx) - assert(mutualCloseTx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx.tx) + assert(mutualCloseTx == bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) // actual test starts here bob ! WatchFundingSpentTriggered(mutualCloseTx.tx) bob ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === mutualCloseTx.tx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == mutualCloseTx.tx) awaitCond(bob.stateName == CLOSED) } @@ -335,7 +342,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice sends an htlc to bob val (ra1, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.isDefined) @@ -346,16 +353,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // scenario 1: bob claims the htlc output from the commit tx using its preimage val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33))) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessFromCommitTx) - val fulfill1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill1.htlc === htlca1) - assert(fulfill1.result.paymentPreimage === ra1) + val fulfill1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] + assert(fulfill1.htlc == htlca1) + assert(fulfill1.result.paymentPreimage == ra1) // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessHtlcSuccess(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33), Transactions.DefaultCommitmentFormat)) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessTx) - val fulfill2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill2.htlc === htlca1) - assert(fulfill2.result.paymentPreimage === ra1) + val fulfill2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] + assert(fulfill2.htlc == htlca1) + assert(fulfill2.result.paymentPreimage == ra1) assert(alice.stateData == initialState) // this was a no-op } @@ -363,11 +370,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testLocalCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) // alice sends an htlc to bob val (_, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -379,28 +386,28 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size === 1) + assert(closingState.htlcTxs.size == 1) assert(getHtlcSuccessTxs(closingState).isEmpty) - assert(getHtlcTimeoutTxs(closingState).length === 1) + assert(getHtlcTimeoutTxs(closingState).length == 1) val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head.tx - assert(closingState.claimHtlcDelayedTxs.length === 0) + assert(closingState.claimHtlcDelayedTxs.length == 0) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === closingState.commitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == closingState.commitTx) assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.toSelfDelay.toInt) assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash) // htlcs below dust will never reach the chain, once the commit tx is confirmed we can consider them failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca2) - relayerA.expectNoMessage(100 millis) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlca2) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet === Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca1) - relayerA.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlca1) + alice2relayer.expectNoMessage(100 millis) // We claim the htlc-delayed output now that the HTLC tx has been confirmed. val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] Transaction.correctlySpends(claimHtlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length === 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 1) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcDelayedTx.tx) awaitCond(alice.stateName == CLOSED) @@ -434,32 +441,32 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size === 4) + assert(closingState.htlcTxs.size == 4) assert(getHtlcSuccessTxs(closingState).isEmpty) val htlcTimeoutTxs = getHtlcTimeoutTxs(closingState).map(_.tx) - assert(htlcTimeoutTxs.length === 4) - assert(closingState.claimHtlcDelayedTxs.length === 0) + assert(htlcTimeoutTxs.length == 4) + assert(closingState.claimHtlcDelayedTxs.length == 0) // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, htlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingState.commitTx) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === dust) - relayerA.expectNoMessage(250 millis) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == dust) + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, htlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, htlcTimeoutTxs(3)) - val forwardedFail4 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) === Set(htlca1, htlca2, htlca3, htlca4)) - relayerA.expectNoMessage(250 millis) + val forwardedFail4 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) == Set(htlca1, htlca2, htlca3, htlca4)) + alice2relayer.expectNoMessage(250 millis) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length === 4) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) val claimHtlcDelayedTxs = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, claimHtlcDelayedTxs(0).tx) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcDelayedTxs(1).tx) @@ -471,7 +478,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (local commit with htlcs only signed by local)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // alice sends an htlc val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) @@ -489,10 +496,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv WatchTxConfirmedTriggered (local commit with fulfill only signed by local)") { f => @@ -500,9 +507,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob sends an htlc val (r, htlc) = addHtlc(110000000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommitTx.txOut.size === 3) // 2 main outputs + 1 htlc + assert(aliceCommitTx.txOut.size == 3) // 2 main outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature alice ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) @@ -512,15 +519,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel val closingState = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txid === aliceCommitTx.txid) + assert(closingState.commitTx.txid == aliceCommitTx.txid) assert(getHtlcTimeoutTxs(closingState).isEmpty) - assert(getHtlcSuccessTxs(closingState).length === 1) + assert(getHtlcSuccessTxs(closingState).length == 1) } test("recv WatchTxConfirmedTriggered (local commit with fail not acked by remote)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val (_, htlc) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) failHtlc(htlc.id, bob, alice, bob2alice, alice2bob) @@ -535,7 +542,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // note that alice doesn't receive the last revocation // then we make alice unilaterally close the channel val closingState = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txOut.length === 2) // htlc has been removed + assert(closingState.commitTx.txOut.length == 2) // htlc has been removed // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] @@ -544,10 +551,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (local commit)") { f => @@ -569,12 +576,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.commitTx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.commitTx.txid) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcTimeoutTx.input.outPoint.index) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.commitTx) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcTimeoutTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.commitTx.txid) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) // the htlc transaction confirms, so we publish a 3rd-stage transaction alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 1, closingState.commitTx) @@ -582,8 +589,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.nonEmpty) val beforeSecondRestart = alice.stateData.asInstanceOf[DATA_CLOSING] val claimHtlcTimeoutTx = beforeSecondRestart.localCommitPublished.get.claimHtlcDelayedTxs.head - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcTimeoutTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) // simulate another node restart alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) @@ -592,16 +599,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutTx.tx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcTimeoutTx.tx.txid) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) } test("recv WatchTxConfirmedTriggered (remote commit with htlcs only signed by local in next remote commit)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // alice sends an htlc val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) @@ -618,7 +625,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) } @@ -635,7 +642,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(closingState.claimHtlcTxs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit } @@ -643,7 +650,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.channelFeatures === ChannelFeatures()) + assert(initialState.commitments.channelFeatures == ChannelFeatures()) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -656,14 +663,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with txListener.expectMsgType[TransactionPublished] alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) awaitCond(alice.stateName == CLOSED) } test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -681,7 +688,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + assert(initialState.commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors @@ -699,7 +706,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) @@ -713,27 +720,27 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimHtlcTxs.size === 3) + assert(closingState.claimHtlcTxs.size == 3) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) - assert(claimHtlcTimeoutTxs.length === 3) + assert(claimHtlcTimeoutTxs.length == 3) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === Set(htlca1, htlca2, htlca3)) - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == Set(htlca1, htlca2, htlca3)) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -754,7 +761,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. val (r1, htlc1) = addHtlc(110000000 msat, CltvExpiryDelta(48), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. val (_, htlc2) = addHtlc(95000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice) @@ -763,39 +770,39 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the first commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length === 3) // two main outputs + 1 HTLC + assert(bobCommitTx.txOut.length == 3) // two main outputs + 1 HTLC val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size === 1) + assert(closingState.claimHtlcTxs.size == 1) assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet assert(getClaimHtlcTimeoutTxs(closingState).isEmpty) // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.claimMainOutputTx.get.tx) val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get).head.tx Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.txInfo.tx === claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong === htlc1.cltvExpiry.toLong) + assert(publishHtlcSuccessTx.txInfo.tx == claimHtlcSuccessTx) + assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong == htlc1.cltvExpiry.toLong) // Alice resets watches on all relevant transactions. - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.claimMainOutputTx.get.tx.txid) val watchHtlcSuccess = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchHtlcSuccess.txId === bobCommitTx.txid) - assert(watchHtlcSuccess.outputIndex === claimHtlcSuccessTx.txIn.head.outPoint.index) + assert(watchHtlcSuccess.txId == bobCommitTx.txid) + assert(watchHtlcSuccess.outputIndex == claimHtlcSuccessTx.txIn.head.outPoint.index) alice2blockchain.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // The second htlc was not included in the commit tx published on-chain, so we can consider it failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet === Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet == Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (remote commit)") { f => @@ -817,18 +824,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) val publishHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcTimeoutTx.txInfo.tx === htlcTimeoutTx.tx) - assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong === htlca.cltvExpiry.toLong) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcTimeoutTx.input.outPoint.index) + assert(publishHtlcTimeoutTx.txInfo.tx == htlcTimeoutTx.tx) + assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong == htlca.cltvExpiry.toLong) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) } private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) @@ -848,11 +855,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(getClaimHtlcTimeoutTxs(closingState).length === 3) + assert(getClaimHtlcTimeoutTxs(closingState).length == 3) (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) } @@ -860,23 +867,23 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures()) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -886,17 +893,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -906,17 +913,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -925,7 +932,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. val (r1, htlc1) = addHtlc(110000000 msat, CltvExpiryDelta(64), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. val (_, htlc2) = addHtlc(95000000 msat, CltvExpiryDelta(32), alice, bob, alice2bob, bob2alice) @@ -937,41 +944,41 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the next commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size === 2) + assert(closingState.claimHtlcTxs.size == 2) assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).length === 1) + assert(getClaimHtlcTimeoutTxs(closingState).length == 1) val claimHtlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head.tx // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.claimMainOutputTx.get.tx) val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get).head.tx Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.txInfo.tx === claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong === htlc1.cltvExpiry.toLong) + assert(publishHtlcSuccessTx.txInfo.tx == claimHtlcSuccessTx) + assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong == htlc1.cltvExpiry.toLong) val publishHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcTimeoutTx.txInfo.tx === claimHtlcTimeoutTx) - assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong === htlc2.cltvExpiry.toLong) + assert(publishHtlcTimeoutTx.txInfo.tx == claimHtlcTimeoutTx) + assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong == htlc2.cltvExpiry.toLong) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.claimMainOutputTx.get.tx.txid) val watchHtlcs = alice2blockchain.expectMsgType[WatchOutputSpent] :: alice2blockchain.expectMsgType[WatchOutputSpent] :: Nil - watchHtlcs.foreach(ws => assert(ws.txId === bobCommitTx.txid)) - assert(watchHtlcs.map(_.outputIndex).toSet === Set(claimHtlcSuccessTx, claimHtlcTimeoutTx).map(_.txIn.head.outPoint.index)) + watchHtlcs.foreach(ws => assert(ws.txId == bobCommitTx.txid)) + assert(watchHtlcs.map(_.outputIndex).toSet == Set(claimHtlcSuccessTx, claimHtlcTimeoutTx).map(_.txIn.head.outPoint.index)) alice2blockchain.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcTimeoutTx) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -990,17 +997,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx === claimHtlcTimeout.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === claimHtlcTimeout.input.outPoint.index)) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx == claimHtlcTimeout.tx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == claimHtlcTimeout.input.outPoint.index)) } private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Transaction = { import f._ val oldStateData = alice.stateData - assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // This HTLC will be fulfilled. val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) // These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them. @@ -1018,8 +1025,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then we manually replace alice's state with an older one alice.setState(OFFLINE, oldStateData) // then we reconnect them - val aliceInit = Init(TestConstants.Alice.nodeParams.features) - val bobInit = Init(TestConstants.Bob.nodeParams.features) + val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) + val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) // peers exchange channel_reestablish messages @@ -1029,14 +1036,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) + assert(new String(error.data.toArray) == PleasePublishYourCommitment(channelId(alice)).getMessage) // alice now waits for bob to publish its commitment awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs } alice ! WatchFundingSpentTriggered(bobCommitTx) bobCommitTx @@ -1046,19 +1053,19 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures()) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) alice2blockchain.expectNoMessage(250 millis) // alice ignores the htlc-timeout // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainTx) awaitCond(alice.stateName == CLOSED) } @@ -1079,9 +1086,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) alice2blockchain.expectNoMessage(250 millis) // alice ignores the htlc-timeout // actual test starts here @@ -1104,8 +1111,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then we should claim our main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) } case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) @@ -1116,8 +1123,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors - case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs + case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors + case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs } // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc @@ -1132,8 +1139,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) + case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) } // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs @@ -1148,8 +1155,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) - case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) + case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) } // Bob's fourth commit tx doesn't contain any htlc @@ -1162,8 +1169,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) - case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) + case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) + case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) } RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) @@ -1173,7 +1180,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // bob publishes one of his revoked txs val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx @@ -1182,46 +1189,46 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedTx) + assert(rvk.commitTx == bobRevokedTx) if (!channelFeatures.paysDirectlyToWallet) { assert(rvk.claimMainOutputTx.nonEmpty) } assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size === 2) + assert(rvk.htlcPenaltyTxs.size == 2) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs // alice publishes the penalty txs if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.claimMainOutputTx.get.tx) } - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.mainPenaltyTx.get.tx) - assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) === rvk.htlcPenaltyTxs.map(_.tx).toSet) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) + assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) == rvk.htlcPenaltyTxs.map(_.tx).toSet) for (penaltyTx <- penaltyTxs) { Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet - assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid)) + assert(spentOutpoints.forall(_.txid == bobRevokedTx.txid)) if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { - assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors } else if (channelFeatures.paysDirectlyToWallet) { - assert(spentOutpoints.size === bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet } else { - assert(spentOutpoints.size === bobRevokedTx.txOut.size) + assert(spentOutpoints.size == bobRevokedTx.txOut.size) } // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobRevokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) } // alice watches outputs that can be spent by both parties val watchedOutpoints = Seq(alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent]).map(_.outputIndex).toSet - assert(watchedOutpoints === (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.input.outPoint.index).toSet) + assert(watchedOutpoints == (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.input.outPoint.index).toSet) alice2blockchain.expectNoMessage(1 second) (bobRevokedTx, rvk) @@ -1232,20 +1239,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobRevokedTx) + assert(txPublished.tx == bobRevokedTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the revoked commit // once all txs are confirmed, alice can move to the closed state alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, bobRevokedTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobRevokedTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobRevokedTx) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.mainPenaltyTx.get.tx) if (!channelFeatures.paysDirectlyToWallet) { alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, rvk.claimMainOutputTx.get.tx) } alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk.htlcPenaltyTxs(0).tx) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk.htlcPenaltyTxs(1).tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } test("recv WatchFundingSpentTriggered (one revoked tx)") { f => @@ -1267,13 +1274,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => import f._ val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures()) - assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size === revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct + assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = { alice ! WatchFundingSpentTriggered(revokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == revokedCount) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx === revokedTx) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx) // alice publishes penalty txs val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx @@ -1282,13 +1289,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with (claimMain +: mainPenalty +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // alice watches outputs that can be spent by both parties - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === mainPenalty.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == mainPenalty.txIn.head.outPoint.index) val htlcOutpoints = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex).toSet - assert(htlcOutpoints === htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet) + assert(htlcOutpoints == htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet) alice2blockchain.expectNoMessage(1 second) alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last @@ -1307,9 +1314,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, rvk2.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk2.commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk2.htlcPenaltyTxs(0).tx) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk2.htlcPenaltyTxs(1).tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } def testInputRestoredRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { @@ -1327,13 +1334,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.mainPenaltyTx.get.tx) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === htlcPenalty.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobRevokedTx.txid) - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === rvk.mainPenaltyTx.get.input.outPoint.index) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcPenalty.input.outPoint.index)) + rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) + rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcPenalty.tx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) + rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == rvk.mainPenaltyTx.get.input.outPoint.index) + rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcPenalty.input.outPoint.index)) } test("recv INPUT_RESTORED (one revoked tx)") { f => @@ -1351,7 +1358,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testOutputSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat // bob publishes one of his revoked txs @@ -1361,22 +1368,22 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) if (channelFeatures.paysDirectlyToWallet) { assert(rvk.claimMainOutputTx.isEmpty) } else { assert(rvk.claimMainOutputTx.nonEmpty) } assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size === 4) + assert(rvk.htlcPenaltyTxs.size == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs val claimTxsCount = if (channelFeatures.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs (1 to claimTxsCount).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.commitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) } (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties alice2blockchain.expectNoMessage(1 second) @@ -1386,7 +1393,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == fulfilledHtlc.id => - assert(fulfilledHtlc.paymentHash === txInfo.paymentHash) + assert(fulfilledHtlc.paymentHash == txInfo.paymentHash) Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) }.get val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { @@ -1394,29 +1401,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) }.get val bobOutpoints = Seq(bobHtlcSuccessTx1, bobHtlcTimeoutTx).map(_.input.outPoint).toSet - assert(bobOutpoints.size === 2) + assert(bobOutpoints.size == 2) // alice reacts by publishing penalty txs that spend bob's htlc transactions alice ! WatchOutputSpentTriggered(bobHtlcSuccessTx1.tx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 1) val claimHtlcSuccessPenalty1 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last Transaction.correctlySpends(claimHtlcSuccessPenalty1.tx, bobHtlcSuccessTx1.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcSuccessTx1.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcSuccessPenalty1.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx1.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty1.tx) val watchSpent1 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent1.txId === bobHtlcSuccessTx1.tx.txid) - assert(watchSpent1.outputIndex === claimHtlcSuccessPenalty1.input.outPoint.index) + assert(watchSpent1.txId == bobHtlcSuccessTx1.tx.txid) + assert(watchSpent1.outputIndex == claimHtlcSuccessPenalty1.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) alice ! WatchOutputSpentTriggered(bobHtlcTimeoutTx.tx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) val claimHtlcTimeoutPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last Transaction.correctlySpends(claimHtlcTimeoutPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcTimeoutTx.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutPenalty.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTimeoutTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutPenalty.tx) val watchSpent2 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent2.txId === bobHtlcTimeoutTx.tx.txid) - assert(watchSpent2.outputIndex === claimHtlcTimeoutPenalty.input.outPoint.index) + assert(watchSpent2.txId == bobHtlcTimeoutTx.tx.txid) + assert(watchSpent2.outputIndex == claimHtlcTimeoutPenalty.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) // bob RBFs his htlc-success with a different transaction @@ -1427,16 +1434,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcSuccessPenalty2 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last assert(claimHtlcSuccessPenalty1.tx.txid != claimHtlcSuccessPenalty2.tx.txid) Transaction.correctlySpends(claimHtlcSuccessPenalty2.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcSuccessTx2.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcSuccessPenalty2.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx2.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty2.tx) val watchSpent3 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent3.txId === bobHtlcSuccessTx2.txid) - assert(watchSpent3.outputIndex === claimHtlcSuccessPenalty2.input.outPoint.index) + assert(watchSpent3.txId == bobHtlcSuccessTx2.txid) + assert(watchSpent3.outputIndex == claimHtlcSuccessPenalty2.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) // transactions confirm: alice can move to the closed state val remainingHtlcPenaltyTxs = rvk.htlcPenaltyTxs.filterNot(htlcPenalty => bobOutpoints.contains(htlcPenalty.input.outPoint)) - assert(remainingHtlcPenaltyTxs.size === 2) + assert(remainingHtlcPenaltyTxs.size == 2) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk.commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, rvk.mainPenaltyTx.get.tx) if (!channelFeatures.paysDirectlyToWallet) { @@ -1446,11 +1453,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, remainingHtlcPenaltyTxs.last.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, bobHtlcTimeoutTx.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 1, bobHtlcSuccessTx2) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, claimHtlcTimeoutPenalty.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, claimHtlcSuccessPenalty2.tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx)") { f => @@ -1478,17 +1485,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) - assert(rvk.htlcPenaltyTxs.size === 4) + assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + assert(rvk.htlcPenaltyTxs.size == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs (1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) // 2 main outputs and 4 htlcs - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties alice2blockchain.expectNoMessage(1 second) @@ -1497,12 +1504,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) => val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get - assert(Crypto.sha256(preimage) === txInfo.paymentHash) + assert(Crypto.sha256(preimage) == txInfo.paymentHash) Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) => Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) } - assert(bobHtlcTxs.map(_.input.outPoint).size === 4) + assert(bobHtlcTxs.map(_.input.outPoint).size == 4) val bobHtlcTx = Transaction( 2, Seq( @@ -1527,34 +1534,34 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4) val claimHtlcDelayedPenaltyTxs = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs val spentOutpoints = Set(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)) - assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet === spentOutpoints) + assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints) claimHtlcDelayedPenaltyTxs.foreach(claimHtlcPenalty => Transaction.correctlySpends(claimHtlcPenalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTx.txid) val publishedPenaltyTxs = Set( alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx] ) - assert(publishedPenaltyTxs.map(_.tx) === claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) + assert(publishedPenaltyTxs.map(_.tx) == claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) val watchedOutpoints = Seq( alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent] ).map(w => OutPoint(w.txId.reverse, w.outputIndex)).toSet - assert(watchedOutpoints === spentOutpoints) + assert(watchedOutpoints == spentOutpoints) alice2blockchain.expectNoMessage(1 second) } private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val initOutputCount = channelFeatures.commitmentFormat match { case _: AnchorOutputsCommitmentFormat => 4 case DefaultCommitmentFormat => 2 } - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) // bob's second commit tx contains 2 incoming htlcs val (bobRevokedTx, htlcs1) = { @@ -1562,7 +1569,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc2) = addHtlc(20000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.size === initOutputCount + 2) + assert(bobCommitTx.txOut.size == initOutputCount + 2) (bobCommitTx, Seq(htlc1, htlc2)) } @@ -1572,27 +1579,27 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc4) = addHtlc(18000000 msat, alice, bob, alice2bob, bob2alice) failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount + 3) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount + 3) Seq(htlc3, htlc4) } // alice's first htlc has been failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc === htlcs1.head) - relayerA.expectNoMessage(1 second) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc == htlcs1.head) + alice2relayer.expectNoMessage(1 second) // bob publishes one of his revoked txs which quickly confirms alice ! WatchFundingSpentTriggered(bobRevokedTx) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, bobRevokedTx) - awaitCond(alice.stateName === CLOSING) + awaitCond(alice.stateName == CLOSING) // alice should fail all pending htlcs val htlcFails = Seq( - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]] + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]] ).map(_.htlc).toSet - assert(htlcFails === Set(htlcs1(1), htlcs2(0), htlcs2(1))) - relayerA.expectNoMessage(1 second) + assert(htlcFails == Set(htlcs1(1), htlcs2(0), htlcs2(1))) + alice2relayer.expectNoMessage(1 second) } test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs)") { f => @@ -1611,7 +1618,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments + val bobCommitments = bob.stateData.asInstanceOf[PersistentChannelData].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index) @@ -1619,7 +1626,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! ChannelReestablish(channelId(bob), 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === FundingTxSpent(channelId(alice), initialState.spendingTxs.head).getMessage) + assert(new String(error.data.toArray) == FundingTxSpent(channelId(alice), initialState.spendingTxs.head).getMessage) } test("recv CMD_CLOSE") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala index 84da3119e0..de613699fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala @@ -26,7 +26,7 @@ class ChaCha20Poly1305Spec extends AnyFunSuite { val key: ByteVector = hex"85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b" val data: ByteVector = ByteVector.view("Cryptographic Forum Research Group".getBytes("UTF-8")) val mac = Poly1305.mac(key, data) - assert(mac === hex"a8061dc1305136c6c22b8baf0c0127a9") + assert(mac == hex"a8061dc1305136c6c22b8baf0c0127a9") } test("Poly1305 #2") { @@ -34,7 +34,7 @@ class ChaCha20Poly1305Spec extends AnyFunSuite { val key: ByteVector = ByteVector.view("this is 32-byte key for Poly1305".getBytes()) val data: ByteVector = ByteVector.view("Hello world!".getBytes("UTF-8")) val mac = Poly1305.mac(key, data) - assert(mac === hex"a6f745008f81c916a20dcc74eef2b2f0") + assert(mac == hex"a6f745008f81c916a20dcc74eef2b2f0") } test("Chacha20Poly1305 IETF RFC7539 tests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala index 0112c5a566..760b9ff414 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala index 3daa3dfe90..a390d64187 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.scalatest.funsuite.AnyFunSuite import scodec.bits.HexStringSyntax diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala index 29c4917c59..a5f5d240d1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.crypto.RandomSpec.entropyScore import org.bouncycastle.crypto.engines.ChaCha7539Engine import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} @@ -28,7 +28,7 @@ class RandomSpec extends AnyFunSuiteLike { test("random long generation") { for (rng <- Seq(new WeakRandom(), new StrongRandom())) { val randomNumbers = (1 to 1000).map(_ => rng.nextLong()) - assert(randomNumbers.toSet.size === 1000) + assert(randomNumbers.toSet.size == 1000) val entropy = randomNumbers.foldLeft(0.0) { case (current, next) => current + entropyScore(next) } / 1000 assert(entropy >= 0.98) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala index 3c16846cc8..c4ad17b189 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -78,7 +78,7 @@ class ShaChainSpec extends AnyFunSuite { test("generate and receive hashes") { val result: List[ByteVector32] = (for (i <- 0 until 50) yield ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - i)).toList - assert(result === expected) + assert(result == expected) var receiver = ShaChain.empty for (i <- 0 until 1000) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 6b6f468199..b8a9aeb54a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ @@ -93,7 +93,7 @@ class SphinxSpec extends AnyFunSuite { ) for ((expected, payload) <- testCases) { - assert(peekPayloadLength(payload) === expected) + assert(peekPayloadLength(payload) == expected) } } @@ -110,7 +110,7 @@ class SphinxSpec extends AnyFunSuite { ) for ((expected, packet) <- testCases) { - assert(packet.isLastPacket === expected) + assert(packet.isLastPacket == expected) } } @@ -129,7 +129,7 @@ class SphinxSpec extends AnyFunSuite { for ((packet, expected) <- badOnions zip expected) { val Left(onionErr) = peel(privKeys.head, associatedData, packet) - assert(onionErr === expected) + assert(onionErr == expected) } } @@ -244,22 +244,22 @@ class SphinxSpec extends AnyFunSuite { val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) - assert(packet1.length === FailurePacket.PacketLength) + assert(packet1.length == FailurePacket.PacketLength) val Success(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted1 === expected) + assert(decrypted1 == expected) val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) - assert(packet2.length === FailurePacket.PacketLength) + assert(packet2.length == FailurePacket.PacketLength) val Success(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted2 === expected) + assert(decrypted2 == expected) val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) - assert(packet3.length === FailurePacket.PacketLength) + assert(packet3.length == FailurePacket.PacketLength) val Success(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted3 === expected) + assert(decrypted3 == expected) } test("decrypt invalid failure message") { @@ -304,24 +304,24 @@ class SphinxSpec extends AnyFunSuite { // node #4 want to reply with an error message val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure) - assert(error === hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") + assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") // error sent back to 3, 2, 1 and 0 val error1 = FailurePacket.wrap(error, sharedSecret3) - assert(error1 === hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") + assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") val error2 = FailurePacket.wrap(error1, sharedSecret2) - assert(error2 === hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") + assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") val error3 = FailurePacket.wrap(error2, sharedSecret1) - assert(error3 === hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") + assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") val error4 = FailurePacket.wrap(error3, sharedSecret0) - assert(error4 === hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") + assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // origin parses error packet and can see that it comes from node #4 val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) - assert(pubkey === publicKeys(4)) - assert(failure === TemporaryNodeFailure) + assert(pubkey == publicKeys(4)) + assert(failure == TemporaryNodeFailure) } } @@ -335,7 +335,7 @@ class SphinxSpec extends AnyFunSuite { for (error <- errors) { val wrapped = FailurePacket.wrap(error, sharedSecret) - assert(wrapped.length === FailurePacket.PacketLength) + assert(wrapped.length == FailurePacket.PacketLength) } } @@ -367,34 +367,34 @@ class SphinxSpec extends AnyFunSuite { // origin parses error packet and can see that it comes from node #2 val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) - assert(pubkey === publicKeys(2)) - assert(failure === InvalidRealm) + assert(pubkey == publicKeys(2)) + assert(failure == InvalidRealm) } } test("create blinded route (reference test vector)") { val sessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads) - assert(blindedRoute.introductionNode.publicKey === publicKeys(0)) - assert(blindedRoute.introductionNodeId === publicKeys(0)) - assert(blindedRoute.introductionNode.blindedPublicKey === PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) - assert(blindedRoute.introductionNode.blindingEphemeralKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) - assert(blindedRoute.introductionNode.encryptedPayload === hex"af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712") - assert(blindedRoute.blindedNodeIds === Seq( + assert(blindedRoute.introductionNode.publicKey == publicKeys(0)) + assert(blindedRoute.introductionNodeId == publicKeys(0)) + assert(blindedRoute.introductionNode.blindedPublicKey == PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) + assert(blindedRoute.introductionNode.blindingEphemeralKey == PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) + assert(blindedRoute.introductionNode.encryptedPayload == hex"af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712") + assert(blindedRoute.blindedNodeIds == Seq( PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2"), PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) === Seq( + assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) == Seq( PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) - assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) === Seq( + assert(blindedRoute.encryptedPayloads == blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) + assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) == Seq( hex"146c9694ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739e368aa602e43e07e14bfb81d66e1e295f848b6f15ee6483005abb830f4ef08a9da6", hex"8ad7d5d448f15208417a1840f82274101b3c254c24b1b49fd676fd0c4293c9aa66ed51da52579e934a869f016f213044d1b13b63bf586e9c9832106b59", hex"52a45a884542d180e76fe84fc13e71a01f65d943ff89aed29b94644a91b037b9143cfda8f1ff25ba61c37108a5ae57d9ddc5ab688ee8b2f9f6bd94522c", @@ -403,31 +403,31 @@ class SphinxSpec extends AnyFunSuite { // The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key. val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.introductionNode.blindingEphemeralKey, blindedRoute.encryptedPayloads(0)) - assert(payload0 === routeBlindingPayloads(0)) - assert(ephKey1 === PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0")) + assert(payload0 == routeBlindingPayloads(0)) + assert(ephKey1 == PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.blindedNodeIds(1)) + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey == blindedRoute.blindedNodeIds(1)) val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) - assert(payload1 === routeBlindingPayloads(1)) - assert(ephKey2 === PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd")) + assert(payload1 == routeBlindingPayloads(1)) + assert(ephKey2 == PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey === blindedRoute.blindedNodeIds(2)) + assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey == blindedRoute.blindedNodeIds(2)) val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)) - assert(payload2 === routeBlindingPayloads(2)) - assert(ephKey3 === PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc")) + assert(payload2 == routeBlindingPayloads(2)) + assert(ephKey3 == PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.blindedNodeIds(3)) + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey == blindedRoute.blindedNodeIds(3)) val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) - assert(payload3 === routeBlindingPayloads(3)) - assert(ephKey4 === PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a")) + assert(payload3 == routeBlindingPayloads(3)) + assert(ephKey4 == PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a")) // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.blindedNodeIds(4)) + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey == blindedRoute.blindedNodeIds(4)) val Success((payload4, _)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) - assert(payload4 === routeBlindingPayloads(4)) + assert(payload4 == routeBlindingPayloads(4)) } test("concatenate blinded routes (reference test vector)") { @@ -440,7 +440,7 @@ class SphinxSpec extends AnyFunSuite { hex"010f000000000000000000000000000000 061000112233445566778899aabbccddeeff" ) val blindedRoute = RouteBlinding.create(sessionKey, publicKeys.drop(2), payloads) - assert(blindedRoute.blindingKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) + assert(blindedRoute.blindingKey == PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) (blindedRoute.blindingKey, blindedRoute, payloads) } // The sender also wants to use route blinding to reach the introduction point. @@ -454,15 +454,15 @@ class SphinxSpec extends AnyFunSuite { (RouteBlinding.create(sessionKey, publicKeys.take(2), payloads), payloads) } val blindedRoute = BlindedRoute(publicKeys(0), blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes ++ blindedRouteEnd.blindedNodes) - assert(blindedRoute.blindingKey === PublicKey(hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")) - assert(blindedRoute.blindedNodeIds === Seq( + assert(blindedRoute.blindingKey == PublicKey(hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")) + assert(blindedRoute.blindedNodeIds == Seq( PublicKey(hex"0303176d13958a8a59d59517a6223e12cf291ba5f65c8011efcdca0a52c3850abc"), PublicKey(hex"03adbdd3c0fb69641e96de2d5ac923ffc0910d3ed4dfe2314609fae61a71df4da2"), PublicKey(hex"021026e6369e42b7f6d723c0c56a3e0b4d67111f07685bd03e9fa6d93ac6bb6dbe"), PublicKey(hex"02ba3db3fe7f1ed28c4d82f28cf358373cbf3241a16aba265b1b6fb26f094c0c7f"), PublicKey(hex"0379d4ca14cb19e2f7bcb217d36267e3d03b027bc4228923967f5b2e32cbb763c1"), )) - assert(blindedRoute.encryptedPayloads === Seq( + assert(blindedRoute.encryptedPayloads == Seq( hex"31da0d438752ed0f19ccd970a386ead7155fd187becd4e1770d561dffdb03d3568dac746dde98725f146582cb040207e8b6c070e28d707564a4dd9fb53f9274ad69d09add393b509a2fa42df5055d7c8aeda5881d5aa", hex"d9dfa92f898dc8e37b73c944aa4205f225337b2edde67623e775c79e2bcf395dc205004aa07fdc65712afa5c2687aff9bb3d5e6af7c89cc94f23f962a27844ce7629773f9413ebcf131dbc35818410df207f29b013b0", hex"30015dcdcbce70bdcd0125be8ccd541b101d95bcb049ccfc737f91c98cc139cb6f16354ec5a38e77eca769c2245ac4467524d6", @@ -472,35 +472,35 @@ class SphinxSpec extends AnyFunSuite { // The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key. val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.blindingKey, blindedRoute.encryptedPayloads(0)) - assert(payload0 === payloadsStart(0)) - assert(ephKey1 === PublicKey(hex"02be4b436dbc6cfa43d7d5652bc630ffdaf0dac93e6682db7950828506055ad1a7")) + assert(payload0 == payloadsStart(0)) + assert(ephKey1 == PublicKey(hex"02be4b436dbc6cfa43d7d5652bc630ffdaf0dac93e6682db7950828506055ad1a7")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.blindedNodeIds(1)) + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey == blindedRoute.blindedNodeIds(1)) val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) - assert(payload1 === payloadsStart(1)) - assert(ephKey2 === PublicKey(hex"03fb82254d740754efddc3318674f4e26cefcb8dec42a3910c08c64d19f25e50b7")) + assert(payload1 == payloadsStart(1)) + assert(ephKey2 == PublicKey(hex"03fb82254d740754efddc3318674f4e26cefcb8dec42a3910c08c64d19f25e50b7")) // NB: this node finds a blinding override and will transmit that instead of ephKey2 to the next node. assert(payload1.containsSlice(blindingOverride.value)) // The next node must be given the blinding override to derive the private key used to unwrap the onion and decrypt its encrypted payload. assert(RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)).isFailure) - assert(RouteBlinding.derivePrivateKey(privKeys(2), blindingOverride).publicKey === blindedRoute.blindedNodeIds(2)) + assert(RouteBlinding.derivePrivateKey(privKeys(2), blindingOverride).publicKey == blindedRoute.blindedNodeIds(2)) val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), blindingOverride, blindedRoute.encryptedPayloads(2)) - assert(payload2 === payloadsEnd(0)) - assert(ephKey3 === PublicKey(hex"03932f4ab7605e8c046b5677becd4d61fdfdc8b9d10f1e9c3080ced0d64fd76931")) + assert(payload2 == payloadsEnd(0)) + assert(ephKey3 == PublicKey(hex"03932f4ab7605e8c046b5677becd4d61fdfdc8b9d10f1e9c3080ced0d64fd76931")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.blindedNodeIds(3)) + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey == blindedRoute.blindedNodeIds(3)) val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) - assert(payload3 === payloadsEnd(1)) - assert(ephKey4 === PublicKey(hex"037bceb365470d24f8204c622e1b7959c6beeb774c634640de6c8401079159fc58")) + assert(payload3 == payloadsEnd(1)) + assert(ephKey4 == PublicKey(hex"037bceb365470d24f8204c622e1b7959c6beeb774c634640de6c8401079159fc58")) // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.blindedNodeIds(4)) + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey == blindedRoute.blindedNodeIds(4)) val Success((payload4, ephKey5)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) - assert(payload4 === payloadsEnd(2)) - assert(ephKey5 === PublicKey(hex"0339ddfa85a2155fb27e94742885fad85696e54920aa148cb86e00bcb8ee346bd4")) + assert(payload4 == payloadsEnd(2)) + assert(ephKey5 == PublicKey(hex"0339ddfa85a2155fb27e94742885fad85696e54920aa148cb86e00bcb8ee346bd4")) } test("invalid blinded route") { @@ -523,7 +523,7 @@ class SphinxSpec extends AnyFunSuite { // The sender obtains this information (e.g. from a Bolt11 invoice) and prepends two normal hops to reach the introduction node. val nodeIds = publicKeys.take(2) ++ Seq(blindedRoute.introductionNodeId) ++ blindedRoute.subsequentNodes.map(_.blindedPublicKey) - assert(blindedRoute.encryptedPayloads === Seq( + assert(blindedRoute.encryptedPayloads == Seq( hex"36285ee1c0b289eeedf05c9ab66a7b669d92bd3729082c1c42e443d2775b3be7de74b1c64e0cebd0e3a8cddeff2e9acb1ddb62cbb73166723cae905938", hex"14cd98e3f4f29cc7af8624250c5bd14fb5a69be8235748909e754ef43b09b1b424b1bba062d801f8648f28fb1101b9a56dcb1f69d5e1ba7fe584f6a6be", hex"fe7862b65ac8e1c2a319ba558513d97dc237132b22ce4f7439983545e37164d792dc6925a3c7cde855ac824871bd455f2859298456da3ade87d884080d", @@ -552,11 +552,11 @@ class SphinxSpec extends AnyFunSuite { // However it contains a blinding point and encrypted data, which it can decrypt to discover the next node. val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = peel(privKeys(2), associatedData, nextPacket1) val tlvs2 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload2.bits).require.value - assert(tlvs2.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey) === Some(blindingEphemeralKey0)) + assert(tlvs2.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey) == Some(blindingEphemeralKey0)) assert(tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs2, blindingEphemeralKey1)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(2), blindingEphemeralKey0, tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingChannelId].map(_.shortChannelId) === Some(ShortChannelId(1105))) - assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(3))) + assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingChannelId].map(_.shortChannelId) == Some(ShortChannelId(1105))) + assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) == Some(publicKeys(3))) // The fourth hop is a blinded hop. // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to @@ -567,7 +567,7 @@ class SphinxSpec extends AnyFunSuite { val tlvs3 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload3.bits).require.value assert(tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs3, blindingEphemeralKey2)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(3), blindingEphemeralKey1, tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs3.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(4))) + assert(recipientTlvs3.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) == Some(publicKeys(4))) // The fifth hop is the blinded recipient. // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to @@ -577,7 +577,7 @@ class SphinxSpec extends AnyFunSuite { val tlvs4 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload4.bits).require.value assert(tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs4, _)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs4.get[RouteBlindingEncryptedDataTlv.PathId].map(_.data) === associatedData.map(_.bytes)) + assert(recipientTlvs4.get[RouteBlindingEncryptedDataTlv.PathId].map(_.data) == associatedData.map(_.bytes)) assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index 2e915fdd4a..0f0b2a89e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.crypto -import java.nio.charset.Charset - import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated} import akka.io.Tcp import akka.testkit.{TestActorRef, TestFSMRef, TestProbe} @@ -31,6 +29,7 @@ import scodec.Codec import scodec.bits._ import scodec.codecs._ +import java.nio.charset.Charset import scala.annotation.tailrec import scala.concurrent.duration._ @@ -46,7 +45,7 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be val s = Noise.Secp256k1DHFunctions.generateKeyPair(hex"2121212121212121212121212121212121212121212121212121212121212121") } - test("succesfull handshake") { + test("successful handshake") { val pipe = system.actorOf(Props[MyPipe]()) val probe1 = TestProbe() val probe2 = TestProbe() @@ -76,7 +75,7 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be probe1.expectTerminated(pipe) } - test("succesfull handshake with custom serializer") { + test("successful handshake with custom serializer") { case class MyMessage(payload: String) val mycodec: Codec[MyMessage] = ("payload" | scodec.codecs.string32L(Charset.defaultCharset())).as[MyMessage] val pipe = system.actorOf(Props[MyPipe]()) @@ -108,6 +107,52 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be probe1.expectTerminated(pipe) } + test("handle unknown messages") { + sealed trait Message + case object Msg1 extends Message + case object Msg2 extends Message + + val codec1: Codec[Message] = discriminated[Message].by(uint8) + .typecase(1, provide(Msg1)) + + val codec12: Codec[Message] = discriminated[Message].by(uint8) + .typecase(1, provide(Msg1)) + .typecase(2, provide(Msg2)) + + val pipe = system.actorOf(Props[MyPipePull]()) + val probe1 = TestProbe() + val probe2 = TestProbe() + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, codec1)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, codec12)) + pipe ! (initiator, responder) + + awaitCond(initiator.stateName == TransportHandler.WaitingForListener) + awaitCond(responder.stateName == TransportHandler.WaitingForListener) + + initiator ! Listener(probe1.ref) + responder ! Listener(probe2.ref) + + awaitCond(initiator.stateName == TransportHandler.Normal) + awaitCond(responder.stateName == TransportHandler.Normal) + + responder ! Msg1 + probe1.expectMsg(Msg1) + probe1.reply(TransportHandler.ReadAck(Msg1)) + + responder ! Msg2 + probe1.expectNoMessage(2 seconds) // unknown message + + responder ! Msg1 + probe1.expectMsg(Msg1) + probe1.reply(TransportHandler.ReadAck(Msg1)) + + probe1.watch(pipe) + initiator.stop() + responder.stop() + system.stop(pipe) + probe1.expectTerminated(pipe) + } + test("handle messages split in chunks") { val pipe = system.actorOf(Props[MyPipeSplitter]()) val probe1 = TestProbe() @@ -187,12 +232,12 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be } val ciphertexts = loop(Encryptor(enc), 1002) - assert(ciphertexts(0) === hex"cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbcf25d2f214cf9ea1d95") - assert(ciphertexts(1) === hex"72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d0f0d7ad833b1a256a1") - assert(ciphertexts(500) === hex"178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb220f48364f7a4c68bf8") - assert(ciphertexts(501) === hex"1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60ca2c104b56b60e45bd") - assert(ciphertexts(1000) === hex"4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c52b76b29b740f09") - assert(ciphertexts(1001) === hex"2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338b1a16cf4ef2d36") + assert(ciphertexts(0) == hex"cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbcf25d2f214cf9ea1d95") + assert(ciphertexts(1) == hex"72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d0f0d7ad833b1a256a1") + assert(ciphertexts(500) == hex"178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb220f48364f7a4c68bf8") + assert(ciphertexts(501) == hex"1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60ca2c104b56b60e45bd") + assert(ciphertexts(1000) == hex"4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c52b76b29b740f09") + assert(ciphertexts(1001) == hex"2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338b1a16cf4ef2d36") } } @@ -250,6 +295,41 @@ object TransportHandlerSpec { } } + class MyPipePull extends Actor with Stash { + + def receive = { + case (a: ActorRef, b: ActorRef) => + unstashAll() + context watch a + context watch b + context become ready(a, b, aResume = true, bResume = true) + + case msg => stash() + } + + def ready(a: ActorRef, b: ActorRef, aResume: Boolean, bResume: Boolean): Receive = { + case Tcp.Write(data, ack) if sender().path == a.path => + if (bResume) { + b forward Tcp.Received(data) + if (ack != Tcp.NoAck) sender() ! ack + context become ready(a, b, aResume, bResume = false) + } else stash() + case Tcp.ResumeReading if sender().path == b.path => + unstashAll() + context become ready(a, b, aResume, bResume = true) + case Tcp.Write(data, ack) if sender().path == b.path => + if (aResume) { + a forward Tcp.Received(data) + if (ack != Tcp.NoAck) sender() ! ack + context become ready(a, b, aResume = false, bResume) + } else stash() + case Tcp.ResumeReading if sender().path == a.path => + unstashAll() + context become ready(a, b, aResume = true, bResume) + case Terminated(actor) if actor == a || actor == b => context stop self + } + } + // custom supervisor that will stop an actor if it fails class MySupervisor extends Actor { override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala index a3b51a684e..57cc1b0df7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala @@ -18,9 +18,9 @@ package fr.acinq.eclair.crypto.keymanager import java.io.File import java.nio.file.Files -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, DeterministicWallet} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.channel.ChannelConfig import fr.acinq.eclair.crypto.ShaChain @@ -55,16 +55,16 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { assert(keyPath.toString() == "m/1909530642'/1080788911/847211985'/1791010671/1303008749'/34154019'/723973395/767609665") } - def makefundingKeyPath(entropy: ByteVector, isFunder: Boolean): KeyPath = { + def makefundingKeyPath(entropy: ByteVector, isInitiator: Boolean): KeyPath = { val items = for (i <- 0 to 7) yield entropy.drop(i * 4).take(4).toInt(signed = false) & 0xFFFFFFFFL - val last = DeterministicWallet.hardened(if (isFunder) 1L else 0L) + val last = DeterministicWallet.hardened(if (isInitiator) 1L else 0L) KeyPath(items :+ last) } test("test vectors (testnet, funder)") { val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isFunder = true) + val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isInitiator = true) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -81,7 +81,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (testnet, fundee)") { val seed = ByteVector.fromValidHex("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isFunder = false) + val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isInitiator = false) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -98,7 +98,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (mainnet, funder)") { val seed = ByteVector.fromValidHex("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isFunder = true) + val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isInitiator = true) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -115,7 +115,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (mainnet, fundee)") { val seed = ByteVector.fromValidHex("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isFunder = false) + val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isInitiator = false) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala index f6d810324e..3b33f54809 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.crypto.keymanager import java.io.File import java.nio.file.Files -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.{NodeParams, TestUtils} import org.scalatest.funsuite.AnyFunSuite @@ -69,7 +69,7 @@ class LocalNodeKeyManagerSpec extends AnyFunSuite { val (signature, recid) = testKeyManager.signDigest(digest) val recoveredPubkey = Crypto.recoverPublicKey(signature, digest, recid) - assert(recoveredPubkey === testKeyManager.nodeId) + assert(recoveredPubkey == testKeyManager.nodeId) assert(Crypto.verifySignature(digest, signature, testKeyManager.nodeId)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index d61f98d641..9ef2ca1d01 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} import fr.acinq.eclair._ import fr.acinq.eclair.channel.Helpers.Closing.MutualClose @@ -28,6 +28,7 @@ import fr.acinq.eclair.db.jdbc.JdbcUtils.using import fr.acinq.eclair.db.pg.PgAuditDb import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} import fr.acinq.eclair.db.sqlite.SqliteAuditDb +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey @@ -76,9 +77,9 @@ class AuditDbSpec extends AnyFunSuite { val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil) val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = TimestampMilli.now() + 10.minutes) val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil) - val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isFunder = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) - val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isInitiator = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) + val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) val e10 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(20000 msat, randomBytes32()), PaymentRelayed.Part(22000 msat, randomBytes32())), Seq(PaymentRelayed.Part(10000 msat, randomBytes32()), PaymentRelayed.Part(12000 msat, randomBytes32()), PaymentRelayed.Part(15000 msat, randomBytes32())), randomKey().publicKey, 30000 msat) val multiPartPaymentHash = randomBytes32() val now = TimestampMilli.now() @@ -100,12 +101,12 @@ class AuditDbSpec extends AnyFunSuite { db.add(e11) db.add(e12) - assert(db.listSent(from = TimestampMilli(0L), to = TimestampMilli.now() + 15.minute).toSet === Set(e1, e5, e6)) - assert(db.listSent(from = TimestampMilli(100000L), to = TimestampMilli.now() + 1.minute).toList === List(e1)) - assert(db.listReceived(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e2)) - assert(db.listRelayed(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e3, e10, e11, e12)) - assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).size === 1) - assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).head.txType === "mutual") + assert(db.listSent(from = TimestampMilli(0L), to = TimestampMilli.now() + 15.minute).toSet == Set(e1, e5, e6)) + assert(db.listSent(from = TimestampMilli(100000L), to = TimestampMilli.now() + 1.minute).toList == List(e1)) + assert(db.listReceived(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList == List(e2)) + assert(db.listRelayed(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList == List(e3, e10, e11, e12)) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).size == 1) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).head.txType == "mutual") } } @@ -147,7 +148,7 @@ class AuditDbSpec extends AnyFunSuite { db.add(TransactionConfirmed(c4, n4, Transaction(0, Seq.empty, Seq(TxOut(2500 sat, hex"ffffff")), 0))) // doesn't match a published tx // NB: we only count a relay fee for the outgoing channel, no the incoming one. - assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet === Set( + assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet == Set( Stats(channelId = c1, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat), Stats(channelId = c1, direction = "OUT", avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat), Stats(channelId = c2, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat), @@ -208,8 +209,8 @@ class AuditDbSpec extends AnyFunSuite { val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 42001 msat, 1001 msat, randomBytes32(), None) val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 42002 msat, 1002 msat, randomBytes32(), None) val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84003 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil) - val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) migrationCheck( dbs = dbs, @@ -248,7 +249,7 @@ class AuditDbSpec extends AnyFunSuite { targetVersion = SqliteAuditDb.CURRENT_VERSION, postCheck = connection => { // existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default - assert(dbs.audit.listSent(0 unixms, TimestampMilli.now() + 1.minute) === Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) + assert(dbs.audit.listSent(0 unixms, TimestampMilli.now() + 1.minute) == Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) val postMigrationDb = new SqliteAuditDb(connection) @@ -262,7 +263,7 @@ class AuditDbSpec extends AnyFunSuite { // the old record will have the UNKNOWN_UUID but the new ones will have their actual id val expected = Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))), ps1) - assert(postMigrationDb.listSent(0 unixms, TimestampMilli.now() + 1.minute) === expected) + assert(postMigrationDb.listSent(0 unixms, TimestampMilli.now() + 1.minute) == expected) } ) } @@ -270,8 +271,8 @@ class AuditDbSpec extends AnyFunSuite { test("migrate sqlite audit database v2 -> current") { val dbs = TestSqliteDatabases() - val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) migrationCheck( dbs = dbs, @@ -380,11 +381,11 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listSent(50 unixms, 150 unixms).toSet === Set( + assert(migratedDb.listSent(50 unixms, 150 unixms).toSet == Set( ps1.copy(id = pp1.id, recipientAmount = pp1.amount, parts = pp1 :: Nil), ps1.copy(id = pp2.id, recipientAmount = pp2.amount, parts = pp2 :: Nil) )) - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => @@ -396,9 +397,9 @@ class AuditDbSpec extends AnyFunSuite { )) val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(ps2) - assert(postMigrationDb.listSent(155 unixms, 200 unixms) === Seq(ps2)) + assert(postMigrationDb.listSent(155 unixms, 200 unixms) == Seq(ps2)) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) } @@ -479,7 +480,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new PgAuditDb()(dbs.datasource) using(connection.createStatement()) { statement => @@ -487,7 +488,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) case dbs: TestSqliteDatabases => @@ -562,7 +563,7 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => @@ -570,7 +571,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) } @@ -640,7 +641,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) == networkFees) } ) case dbs: TestSqliteDatabases => @@ -698,12 +699,102 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) == networkFees) } ) } } + test("migrate postgres audit database v10 -> current") { + forAllDbs { + case dbs: TestPgDatabases => + migrationCheck( + dbs = dbs, + initializeTables = connection => { + // simulate existing v10 db + using(connection.createStatement()) { statement => + statement.executeUpdate("CREATE SCHEMA audit") + + statement.executeUpdate("CREATE TABLE audit.sent (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, recipient_amount_msat BIGINT NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash TEXT NOT NULL, payment_preimage TEXT NOT NULL, recipient_node_id TEXT NOT NULL, to_channel_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.received (amount_msat BIGINT NOT NULL, payment_hash TEXT NOT NULL, from_channel_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.relayed (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, channel_id TEXT NOT NULL, direction TEXT NOT NULL, relay_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.transactions_published (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, mining_fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.transactions_confirmed (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + + statement.executeUpdate("CREATE TABLE audit.channel_errors (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal BOOLEAN NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON audit.sent(timestamp)") + statement.executeUpdate("CREATE INDEX received_timestamp_idx ON audit.received(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_timestamp_idx ON audit.relayed(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_payment_hash_idx ON audit.relayed(payment_hash)") + statement.executeUpdate("CREATE INDEX relayed_trampoline_timestamp_idx ON audit.relayed_trampoline(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_trampoline_payment_hash_idx ON audit.relayed_trampoline(payment_hash)") + statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON audit.channel_events(timestamp)") + statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON audit.channel_errors(timestamp)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON audit.channel_updates(node_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") + statement.executeUpdate("CREATE INDEX metrics_status_idx ON audit.path_finding_metrics(status)") + statement.executeUpdate("CREATE INDEX metrics_timestamp_idx ON audit.path_finding_metrics(timestamp)") + statement.executeUpdate("CREATE INDEX metrics_mpp_idx ON audit.path_finding_metrics(is_mpp)") + statement.executeUpdate("CREATE INDEX metrics_name_idx ON audit.path_finding_metrics(experiment_name)") + statement.executeUpdate("CREATE INDEX transactions_published_timestamp_idx ON audit.transactions_published(timestamp)") + statement.executeUpdate("CREATE INDEX transactions_confirmed_timestamp_idx ON audit.transactions_confirmed(timestamp)") + + setVersion(statement, "audit", 10) + } + + using(connection.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setLong(1, 214000) + statement.setLong(2, 345) + statement.setString(3, "FAILURE") + statement.setLong(4, 520) + statement.setTimestamp(5, TimestampSecond(1651053434L).toSqlTimestamp) + statement.setBoolean(6, true) + statement.setString(7, "experiment-a") + statement.setString(8, "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c") + statement.executeUpdate() + } + using(connection.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setLong(1, 35000) + statement.setLong(2, 43) + statement.setString(3, "SUCCESS") + statement.setLong(4, 2043) + statement.setTimestamp(5, TimestampSecond(1651054567L).toSqlTimestamp) + statement.setBoolean(6, false) + statement.setString(7, "experiment-b") + statement.setString(8, "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f") + statement.executeUpdate() + } + }, + dbName = PgAuditDb.DB_NAME, + targetVersion = PgAuditDb.CURRENT_VERSION, + postCheck = connection => { + val migratedDb = dbs.audit + using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } + using(connection.prepareStatement(s"SELECT amount_msat, status, experiment_name, recipient_node_id FROM audit.path_finding_metrics ORDER BY timestamp")) { statement => + val result = statement.executeQuery() + assert(result.next()) + assert(result.getLong(1) == 214000) + assert(result.getString(2) == "FAILURE") + assert(result.getString(3) == "experiment-a") + assert(result.getString(4) == "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c") + assert(result.next()) + assert(result.getLong(1) == 35000) + assert(result.getString(2) == "SUCCESS") + assert(result.getString(3) == "experiment-b") + assert(result.getString(4) == "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f") + assert(!result.next()) + } + } + ) + case _: TestSqliteDatabases => () + } + } + test("ignore invalid values in the DB") { forAllDbs { dbs => val db = dbs.audit @@ -744,7 +835,7 @@ class AuditDbSpec extends AnyFunSuite { statement.executeUpdate() } - assert(db.listRelayed(0 unixms, 40 unixms) === Nil) + assert(db.listRelayed(0 unixms, 40 unixms) == Nil) } } @@ -754,13 +845,49 @@ class AuditDbSpec extends AnyFunSuite { val scid = ShortChannelId(123) val remoteNodeId = randomKey().publicKey val u = Announcements.makeChannelUpdate(randomBytes32(), randomKey(), remoteNodeId, scid, CltvExpiryDelta(56), 2000 msat, 1000 msat, 999, 1000000000 msat) - dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, scid, remoteNodeId, u)) + dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, remoteNodeId, u)) } } test("add experiment metrics") { forAllDbs { dbs => - dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(100000000 msat, 3000 msat, status = "SUCCESS", 37 millis, TimestampMilli.now(), isMultiPart = false, "my-test-experiment", randomKey().publicKey)) + val isPg = dbs.isInstanceOf[TestPgDatabases] + val hints = Some(Seq(Seq(ExtraHop( + PublicKey(hex"033f2d90d6ba1f771e4b3586b35cc9f825cfcb7cdd7edaa2bfd63f0cb81b17580e"), + ShortChannelId(1), + 1000 msat, + 100, + CltvExpiryDelta(144) + ), ExtraHop( + PublicKey(hex"02c15a88ff263cec5bf79c315b17b7f2e083f71d62a880e30281faaac0898cb2b7"), + ShortChannelId(2), + 900 msat, + 200, + CltvExpiryDelta(12) + )), Seq(ExtraHop( + PublicKey(hex"026ec3e3438308519a75ca4496822a6c1e229174fbcaadeeb174704c377112c331"), + ShortChannelId(3), + 800 msat, + 300, + CltvExpiryDelta(78) + )))) + dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(randomBytes32(), 100000000 msat, 3000 msat, status = "SUCCESS", 37 millis, TimestampMilli.now(), isMultiPart = false, "my-test-experiment", randomKey().publicKey, hints)) + + val table = if (isPg) "audit.path_finding_metrics" else "path_finding_metrics" + val hint_column = if (isPg) ", routing_hints" else "" + using(dbs.connection.prepareStatement(s"SELECT amount_msat, status, fees_msat, duration_ms, experiment_name $hint_column FROM $table")) { statement => + val result = statement.executeQuery() + assert(result.next()) + assert(result.getLong(1) == 100000000) + assert(result.getString(2) == "SUCCESS") + assert(result.getLong(3) == 3000) + assert(result.getLong(4) == 37) + assert(result.getString(5) == "my-test-experiment") + if (isPg) { + assert(result.getString(6) == "[[{\"nodeId\": \"033f2d90d6ba1f771e4b3586b35cc9f825cfcb7cdd7edaa2bfd63f0cb81b17580e\", \"feeBase\": 1000, \"shortChannelId\": \"0x0x1\", \"cltvExpiryDelta\": 144, \"feeProportionalMillionths\": 100}, {\"nodeId\": \"02c15a88ff263cec5bf79c315b17b7f2e083f71d62a880e30281faaac0898cb2b7\", \"feeBase\": 900, \"shortChannelId\": \"0x0x2\", \"cltvExpiryDelta\": 12, \"feeProportionalMillionths\": 200}], [{\"nodeId\": \"026ec3e3438308519a75ca4496822a6c1e229174fbcaadeeb174704c377112c331\", \"feeBase\": 800, \"shortChannelId\": \"0x0x3\", \"cltvExpiryDelta\": 78, \"feeProportionalMillionths\": 300}]]") + } + assert(!result.next()) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 111c1e417d..1a8c6e75d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.db import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} +import fr.acinq.eclair.channel.RealScidStatus import fr.acinq.eclair.db.ChannelsDbSpec.{getPgTimestamp, getTimestamp, testCases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.jdbc.JdbcUtils.using @@ -26,9 +27,9 @@ import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} import fr.acinq.eclair.db.pg.{PgChannelsDb, PgUtils} import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._ -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.{CltvExpiry, ShortChannelId, TestDatabases, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -60,7 +61,7 @@ class ChannelsDbSpec extends AnyFunSuite { val channel1 = ChannelCodecsSpec.normal val channel2a = ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(randomBytes32()) - val channel2b = channel2a.modify(_.shortChannelId).setTo(ShortChannelId(189371)) + val channel2b = channel2a.modify(_.shortIds.real).setTo(RealScidStatus.Final(RealShortChannelId(189371))) val commitNumber = 42 val paymentHash1 = ByteVector32.Zeroes @@ -70,16 +71,16 @@ class ChannelsDbSpec extends AnyFunSuite { intercept[SQLException](db.addHtlcInfo(channel1.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel - assert(db.listLocalChannels().toSet === Set.empty) + assert(db.listLocalChannels().toSet == Set.empty) db.addOrUpdateChannel(channel1) db.addOrUpdateChannel(channel1) - assert(db.listLocalChannels() === List(channel1)) + assert(db.listLocalChannels() == List(channel1)) db.addOrUpdateChannel(channel2a) - assert(db.listLocalChannels() === List(channel1, channel2a)) + assert(db.listLocalChannels() == List(channel1, channel2a)) assert(db.getChannel(channel1.channelId).contains(channel1)) assert(db.getChannel(channel2a.channelId).contains(channel2a)) db.addOrUpdateChannel(channel2b) - assert(db.listLocalChannels() === List(channel1, channel2b)) + assert(db.listLocalChannels() == List(channel1, channel2b)) assert(db.getChannel(channel2b.channelId).contains(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) @@ -90,11 +91,11 @@ class ChannelsDbSpec extends AnyFunSuite { db.removeChannel(channel1.channelId) assert(db.getChannel(channel1.channelId).isEmpty) - assert(db.listLocalChannels() === List(channel2b)) + assert(db.listLocalChannels() == List(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) db.removeChannel(channel2b.channelId) assert(db.getChannel(channel2b.channelId).isEmpty) - assert(db.listLocalChannels() === Nil) + assert(db.listLocalChannels() == Nil) } } @@ -195,11 +196,11 @@ class ChannelsDbSpec extends AnyFunSuite { using(sqlite.createStatement()) { statement => assert(getVersion(statement, "channels").contains(targetVersion)) } - assert(db.listLocalChannels().size === testCases.size) + assert(db.listLocalChannels().size == testCases.size) for (testCase <- testCases) { db.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail for (commitmentNumber <- testCase.commitmentNumbers) { - assert(db.listHtlcInfos(testCase.channelId, commitmentNumber).size === testCase.commitmentNumbers.count(_ == commitmentNumber)) + assert(db.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) } } } @@ -207,11 +208,11 @@ class ChannelsDbSpec extends AnyFunSuite { test("migrate channel database v2 -> current") { def postCheck(channelsDb: ChannelsDb): Unit = { - assert(channelsDb.listLocalChannels().size === testCases.filterNot(_.isClosed).size) + assert(channelsDb.listLocalChannels().size == testCases.filterNot(_.isClosed).size) for (testCase <- testCases.filterNot(_.isClosed)) { channelsDb.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail for (commitmentNumber <- testCase.commitmentNumbers) { - assert(channelsDb.listHtlcInfos(testCase.channelId, commitmentNumber).size === testCase.commitmentNumbers.count(_ == commitmentNumber)) + assert(channelsDb.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) } } } @@ -320,13 +321,13 @@ class ChannelsDbSpec extends AnyFunSuite { dbName = PgChannelsDb.DB_NAME, targetVersion = PgChannelsDb.CURRENT_VERSION, postCheck = connection => { - assert(dbs.channels.listLocalChannels().size === testCases.filterNot(_.isClosed).size) + assert(dbs.channels.listLocalChannels().size == testCases.filterNot(_.isClosed).size) testCases.foreach { testCase => - assert(getPgTimestamp(connection, testCase.channelId, "created_timestamp") === testCase.createdTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_sent_timestamp") === testCase.lastPaymentSentTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_received_timestamp") === testCase.lastPaymentReceivedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_connected_timestamp") === testCase.lastConnectedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "closed_timestamp") === testCase.closedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "created_timestamp") == testCase.createdTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_payment_sent_timestamp") == testCase.lastPaymentSentTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_payment_received_timestamp") == testCase.lastPaymentReceivedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_connected_timestamp") == testCase.lastConnectedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "closed_timestamp") == testCase.closedTimestamp) } } ) @@ -362,7 +363,7 @@ object ChannelsDbSpec { val testCases: Seq[TestCase] = for (_ <- 0 until 10) yield { val channelId = randomBytes32() - val data = stateDataCodec.encode(ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(channelId)).require.bytes + val data = channelDataCodec.encode(ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(channelId)).require.bytes TestCase( channelId = channelId, data = data, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala new file mode 100644 index 0000000000..5d1a4fef8f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala @@ -0,0 +1,117 @@ +package fr.acinq.eclair.db + +import akka.actor.ActorSystem +import com.opentable.db.postgres.embedded.EmbeddedPostgres +import com.zaxxer.hikari.HikariConfig +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.migration._ +import fr.acinq.eclair.db.pg.PgUtils.PgLock +import fr.acinq.eclair.db.pg._ +import org.scalatest.Ignore +import org.scalatest.funsuite.AnyFunSuite +import org.sqlite.SQLiteConfig + +import java.io.File +import java.sql.{Connection, DriverManager} +import java.util.UUID +import javax.sql.DataSource + +/** + * To run this test, create a `migration` directory in your project's `user.dir` + * and copy your sqlite files to it (eclair.sqlite, network.sqlite, audit.sqlite). + * Then remove the `Ignore` annotation and run the test. + */ +@Ignore +class DbMigrationSpec extends AnyFunSuite { + + import DbMigrationSpec._ + + test("eclair migration test") { + val sqlite = loadSqlite("migration\\eclair.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgChannelsDb()(postgresDatasource, PgLock.NoLock) + new PgPendingCommandsDb()(postgresDatasource, PgLock.NoLock) + new PgPeersDb()(postgresDatasource, PgLock.NoLock) + new PgPaymentsDb()(postgresDatasource, PgLock.NoLock) + + PgUtils.inTransaction { postgres => + MigrateChannelsDb.migrateAllTables(sqlite, postgres) + MigratePendingCommandsDb.migrateAllTables(sqlite, postgres) + MigratePeersDb.migrateAllTables(sqlite, postgres) + MigratePaymentsDb.migrateAllTables(sqlite, postgres) + assert(CompareChannelsDb.compareAllTables(sqlite, postgres)) + assert(ComparePendingCommandsDb.compareAllTables(sqlite, postgres)) + assert(ComparePeersDb.compareAllTables(sqlite, postgres)) + assert(ComparePaymentsDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("network migration test") { + val sqlite = loadSqlite("migration\\network.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgNetworkDb()(postgresDatasource) + + PgUtils.inTransaction { postgres => + MigrateNetworkDb.migrateAllTables(sqlite, postgres) + assert(CompareNetworkDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("audit migration test") { + val sqlite = loadSqlite("migration\\audit.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgAuditDb()(postgresDatasource) + + PgUtils.inTransaction { postgres => + MigrateAuditDb.migrateAllTables(sqlite, postgres) + assert(CompareAuditDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("full migration") { + // we need to open in read/write because of the getVersion call + val sqlite = SqliteDatabases( + auditJdbc = loadSqlite("migration\\audit.sqlite", readOnly = false), + eclairJdbc = loadSqlite("migration\\eclair.sqlite", readOnly = false), + networkJdbc = loadSqlite("migration\\network.sqlite", readOnly = false), + jdbcUrlFile_opt = None + ) + val postgres = { + val pg = EmbeddedPostgres.start() + val datasource: DataSource = pg.getPostgresDatabase + val hikariConfig = new HikariConfig + hikariConfig.setDataSource(datasource) + PostgresDatabases( + hikariConfig = hikariConfig, + instanceId = UUID.randomUUID(), + lock = PgLock.NoLock, + jdbcUrlFile_opt = None, + readOnlyUser_opt = None, + resetJsonColumns = false, + safetyChecks_opt = None + )(ActorSystem()) + } + val dualDb = DualDatabases(sqlite, postgres) + MigrateDb.migrateAll(dualDb) + CompareDb.compareAll(dualDb) + } + +} + +object DbMigrationSpec { + def loadSqlite(path: String, readOnly: Boolean = true): Connection = { + val sqliteConfig = new SQLiteConfig() + sqliteConfig.setReadOnly(readOnly) + val dbFile = new File(path) + DriverManager.getConnection(s"jdbc:sqlite:$dbFile", sqliteConfig.toProperties) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala new file mode 100644 index 0000000000..a4b81d94ea --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala @@ -0,0 +1,86 @@ +package fr.acinq.eclair.db + +import com.opentable.db.postgres.embedded.EmbeddedPostgres +import com.typesafe.config.{Config, ConfigFactory} +import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec +import fr.acinq.eclair.{TestKitBaseClass, TestUtils} +import org.scalatest.funsuite.AnyFunSuiteLike + +import java.io.File +import java.util.UUID + +class DualDatabasesSpec extends TestKitBaseClass with AnyFunSuiteLike { + + def fixture(driver: String): DualDatabases = { + val pg = EmbeddedPostgres.start() + val config = DualDatabasesSpec.testConfig(pg.getPort, driver) + val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") + datadir.mkdirs() + val instanceId = UUID.randomUUID() + Databases.init(config, instanceId, datadir).asInstanceOf[DualDatabases] + } + + test("sqlite primary") { + val db = fixture("dual-sqlite-primary") + + db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) + assert(db.primary.channels.listLocalChannels().nonEmpty) + awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) + } + + test("postgres primary") { + val db = fixture("dual-postgres-primary") + + db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) + assert(db.primary.channels.listLocalChannels().nonEmpty) + awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) + } +} + +object DualDatabasesSpec { + def testConfig(port: Int, driver: String): Config = + ConfigFactory.parseString( + s""" + |driver = $driver + |postgres { + | database = "" + | host = "localhost" + | port = $port + | username = "postgres" + | password = "" + | readonly-user = "" + | reset-json-columns = false + | pool { + | max-size = 10 // recommended value = number_of_cpu_cores * 2 + | connection-timeout = 30 seconds + | idle-timeout = 10 minutes + | max-life-time = 30 minutes + | } + | lock-type = "lease" // lease or none (do not use none in production) + | lease { + | interval = 5 seconds // lease-interval must be greater than lease-renew-interval + | renew-interval = 2 seconds + | lock-timeout = 5 seconds // timeout for the lock statement on the lease table + | auto-release-at-shutdown = false // automatically release the lock when eclair is stopping + | } + | safety-checks { + | // a set of basic checks on data to make sure we use the correct database + | enabled = false + | max-age { + | local-channels = 3 minutes + | network-nodes = 30 minutes + | audit-relayed = 10 minutes + | } + | min-count { + | local-channels = 10 + | network-nodes = 3000 + | network-channels = 20000 + | } + | } + |} + |dual { + | migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) + | compare-on-restart = false // compare sqlite and postgres dbs on restart + |} + |""".stripMargin) +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index 6a32942112..3d56d767fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.VariableLengthOnion import fr.acinq.eclair.TestDatabases._ @@ -29,7 +29,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestDatabases, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestDatabases, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scala.collection.{SortedMap, mutable} @@ -60,17 +60,17 @@ class NetworkDbSpec extends AnyFunSuite { val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional)) val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, Features.empty) - assert(db.listNodes().toSet === Set.empty) + assert(db.listNodes().toSet == Set.empty) db.addNode(node_1) db.addNode(node_1) // duplicate is ignored - assert(db.getNode(node_1.nodeId) === Some(node_1)) - assert(db.listNodes().size === 1) + assert(db.getNode(node_1.nodeId) == Some(node_1)) + assert(db.listNodes().size == 1) db.addNode(node_2) db.addNode(node_3) db.addNode(node_4) - assert(db.listNodes().toSet === Set(node_1, node_2, node_3, node_4)) + assert(db.listNodes().toSet == Set(node_1, node_2, node_3, node_4)) db.removeNode(node_2.nodeId) - assert(db.listNodes().toSet === Set(node_1, node_3, node_4)) + assert(db.listNodes().toSet == Set(node_1, node_3, node_4)) db.updateNode(node_1) assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) @@ -81,10 +81,10 @@ class NetworkDbSpec extends AnyFunSuite { forAllDbs { dbs => val db = dbs.network val sig = ByteVector64.Zeroes - val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid = ByteVector32.fromValidHex("0001" * 16) db.addChannel(c, txid, Satoshi(42)) - assert(db.listChannels() === SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) + assert(db.listChannels() == SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) } } @@ -104,27 +104,27 @@ class NetworkDbSpec extends AnyFunSuite { val b = generatePubkeyHigherThan(a) val c = generatePubkeyHigherThan(b) - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid_1 = randomBytes32() val txid_2 = randomBytes32() val txid_3 = randomBytes32() val capacity = 10000 sat - assert(db.listChannels().toSet === Set.empty) + assert(db.listChannels().toSet == Set.empty) db.addChannel(channel_1, txid_1, capacity) db.addChannel(channel_1, txid_1, capacity) // duplicate is ignored - assert(db.listChannels().size === 1) + assert(db.listChannels().size == 1) db.addChannel(channel_2, txid_2, capacity) db.addChannel(channel_3, txid_3, capacity) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None, None), channel_2.shortChannelId -> PublicChannel(channel_2, txid_2, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None, None))) db.removeChannel(channel_2.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None, None))) @@ -136,11 +136,11 @@ class NetworkDbSpec extends AnyFunSuite { db.updateChannel(channel_update_1) // duplicate is ignored db.updateChannel(channel_update_2) db.updateChannel(channel_update_3) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2), None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, Some(channel_update_3), None, None))) db.removeChannel(channel_3.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2), None))) } @@ -194,7 +194,7 @@ class NetworkDbSpec extends AnyFunSuite { } } - val shortChannelIds: Seq[ShortChannelId] = (42 to (5000 + 42)).map(i => ShortChannelId(i)) + val shortChannelIds: Seq[RealShortChannelId] = (42 to (5000 + 42)).map(i => RealShortChannelId(i)) test("remove many channels") { forAllDbs { dbs => @@ -210,11 +210,11 @@ class NetworkDbSpec extends AnyFunSuite { val txid = randomBytes32() channels.foreach(ca => db.addChannel(ca, txid, capacity)) updates.foreach(u => db.updateChannel(u)) - assert(db.listChannels().keySet === channels.map(_.shortChannelId).toSet) + assert(db.listChannels().keySet == channels.map(_.shortChannelId).toSet) val toDelete = channels.map(_.shortChannelId).take(1 + Random.nextInt(2500)) db.removeChannels(toDelete) - assert(db.listChannels().keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) + assert(db.listChannels().keySet == (channels.map(_.shortChannelId).toSet -- toDelete)) } } @@ -224,7 +224,7 @@ class NetworkDbSpec extends AnyFunSuite { db.addToPruned(shortChannelIds) shortChannelIds.foreach { id => assert(db.isPruned(id)) } - db.removeFromPruned(ShortChannelId(5)) + db.removeFromPruned(RealShortChannelId(5)) assert(!db.isPruned(ShortChannelId(5))) } } @@ -279,9 +279,9 @@ class NetworkDbSpec extends AnyFunSuite { dbName = SqliteNetworkDb.DB_NAME, targetVersion = SqliteNetworkDb.CURRENT_VERSION, postCheck = _ => { - assert(dbs.network.listNodes().toSet === nodeTestCases.map(_.node).toSet) + assert(dbs.network.listNodes().toSet == nodeTestCases.map(_.node).toSet) // NB: channel updates are not migrated - assert(dbs.network.listChannels().values.toSet === channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, None, None, None)).toSet) + assert(dbs.network.listChannels().values.toSet == channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, None, None, None)).toSet) } ) } @@ -319,9 +319,9 @@ class NetworkDbSpec extends AnyFunSuite { dbName = PgNetworkDb.DB_NAME, targetVersion = PgNetworkDb.CURRENT_VERSION, postCheck = _ => { - assert(dbs.network.listNodes().toSet === nodeTestCases.map(_.node).toSet) + assert(dbs.network.listNodes().toSet == nodeTestCases.map(_.node).toSet) // NB: channel updates are not migrated - assert(dbs.network.listChannels().values.toSet === channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, tc.update_1_opt, tc.update_2_opt, None)).toSet) + assert(dbs.network.listChannels().values.toSet == channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, tc.update_1_opt, tc.update_2_opt, None)).toSet) } ) } @@ -381,7 +381,7 @@ object NetworkDbSpec { val channelTestCases: Seq[ChannelTestCase] = for (_ <- 0 until 10) yield { val a = randomKey() val b = generatePubkeyHigherThan(a) - val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) val channel_update_1_opt = if (Random.nextBoolean()) { Some(Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, channel.shortChannelId, CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, Random.nextBoolean())) } else None diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index e48e245af7..9a0511430a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -16,15 +16,15 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, forAllDbs, migrationCheck} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.jdbc.JdbcUtils.{setVersion, using} import fr.acinq.eclair.db.pg.PgPaymentsDb import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} +import fr.acinq.eclair.router.Router.{ChannelHop, ChannelRelayParams, NodeHop} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, UnknownNextPeer} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TestDatabases, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite @@ -78,15 +78,15 @@ class PaymentsDbSpec extends AnyFunSuite { // add a few rows val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, PaymentType.Standard, 12345 msat, 12345 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Pending) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) db.addOutgoingPayment(ps1) db.addIncomingPayment(i1, preimage1) db.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100 unixms) - assert(db.listIncomingPayments(1 unixms, 1500 unixms) === Seq(pr1)) - assert(db.listOutgoingPayments(1 unixms, 1500 unixms) === Seq(ps1)) + assert(db.listIncomingPayments(1 unixms, 1500 unixms) == Seq(pr1)) + assert(db.listOutgoingPayments(1 unixms, 1500 unixms) == Seq(ps1)) } ) @@ -102,10 +102,10 @@ class PaymentsDbSpec extends AnyFunSuite { val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000 unixms, None, OutgoingPaymentStatus.Pending) val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010 unixms, None, OutgoingPaymentStatus.Failed(Nil, 1050 unixms)) val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060 unixms)) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, 1090 unixms)) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, 1090 unixms)) + val i2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -163,7 +163,7 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, preimage, payment_request, received_msat, created_at, received_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, i1.paymentHash.toArray) statement.setBytes(2, pr1.paymentPreimage.toArray) - statement.setString(3, PaymentRequest.write(i1)) + statement.setString(3, i1.toString) statement.setLong(4, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) statement.setLong(5, pr1.createdAt.toLong) statement.setLong(6, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) @@ -173,9 +173,9 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, i2.paymentHash.toArray) statement.setBytes(2, pr2.paymentPreimage.toArray) - statement.setString(3, PaymentRequest.write(i2)) + statement.setString(3, i2.toString) statement.setLong(4, pr2.createdAt.toLong) - statement.setLong(5, (i2.timestamp + i2.expiry.get).toLong) + statement.setLong(5, (i2.createdAt + i2.relativeExpiry).toLong) statement.executeUpdate() } }, @@ -184,12 +184,12 @@ class PaymentsDbSpec extends AnyFunSuite { postCheck = _ => { val db = dbs.db.payments - assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) - assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3)) + assert(db.getIncomingPayment(i1.paymentHash) == Some(pr1)) + assert(db.getIncomingPayment(i2.paymentHash) == Some(pr2)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3)) - val i3 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) + val i3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) db.addIncomingPayment(i3, pr3.paymentPreimage) val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32(), PaymentType.Standard, 123 msat, 123 msat, alice, 1100 unixms, Some(i3), OutgoingPaymentStatus.Pending) @@ -201,9 +201,9 @@ class PaymentsDbSpec extends AnyFunSuite { db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms)) - assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3, ps4, ps5, ps6)) - assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) === Seq(pr1, pr2, pr3)) - assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) === Seq(pr2)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3, ps4, ps5, ps6)) + assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) == Seq(pr1, pr2, pr3)) + assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) == Seq(pr2)) }) } @@ -213,7 +213,7 @@ class PaymentsDbSpec extends AnyFunSuite { // Test data val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) val parentId = UUID.randomUUID() - val invoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val invoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) val ps1 = OutgoingPayment(id1, id1, Some("42"), randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.REMOTE, "no candy for you", List(HopSummary(hop_ab), HopSummary(hop_bc)), Some(bob))), 1020 unixms)) val ps2 = OutgoingPayment(id2, parentId, Some("42"), paymentHash1, PaymentType.Standard, 1105 msat, 1105 msat, bob, 1010 unixms, Some(invoice1), OutgoingPaymentStatus.Pending) val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060 unixms)) @@ -256,7 +256,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setLong(5, ps2.amount.toLong) statement.setBytes(6, ps2.recipientNodeId.value.toArray) statement.setLong(7, ps2.createdAt.toLong) - statement.setString(8, PaymentRequest.write(invoice1)) + statement.setString(8, invoice1.toString) statement.executeUpdate() } @@ -284,8 +284,8 @@ class PaymentsDbSpec extends AnyFunSuite { targetVersion = SqlitePaymentsDb.CURRENT_VERSION, postCheck = _ => { val db = dbs.db.payments - assert(db.getOutgoingPayment(id1) === Some(ps1)) - assert(db.listOutgoingPayments(parentId) === Seq(ps2, ps3)) + assert(db.getOutgoingPayment(id1) == Some(ps1)) + assert(db.listOutgoingPayments(parentId) == Seq(ps2, ps3)) } ) } @@ -300,10 +300,10 @@ class PaymentsDbSpec extends AnyFunSuite { val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-01T10:15:30.00Z").toEpochMilli), None, OutgoingPaymentStatus.Pending) val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2020-05-14T13:47:21.00Z").toEpochMilli), None, OutgoingPaymentStatus.Failed(Nil, TimestampMilli(Instant.parse("2021-05-15T04:12:40.00Z").toEpochMilli))) val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-28T09:12:05.00Z").toEpochMilli), None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, TimestampMilli.now())) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, TimestampMilli.now())) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = TimestampSecond(Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond)) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, TimestampMilli.now())) + val i2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = TimestampSecond(Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond)) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -331,7 +331,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) statement.setLong(9, sent.createdAt.toLong) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) sent.status match { case s: OutgoingPaymentStatus.Succeeded => statement.setLong(11, s.completedAt.toLong) @@ -347,14 +347,14 @@ class PaymentsDbSpec extends AnyFunSuite { } } - Seq((i1, preimage1), (i2, preimage2)).foreach { case (pr, preimage) => + Seq((i1, preimage1), (i2, preimage2)).foreach { case (invoice, preimage) => using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, pr.paymentHash.toHex) + statement.setString(1, invoice.paymentHash.toHex) statement.setString(2, preimage.toHex) statement.setString(3, PaymentType.Standard) - statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toTimestampMilli.toLong) + statement.setString(4, invoice.toString) + statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(6, (invoice.createdAt + invoice.relativeExpiry).toTimestampMilli.toLong) statement.executeUpdate() } } @@ -362,7 +362,7 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update => update.setLong(1, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) update.setLong(2, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) - update.setString(3, pr1.paymentRequest.paymentHash.toHex) + update.setString(3, pr1.invoice.paymentHash.toHex) val updated = update.executeUpdate() if (updated == 0) { throw new IllegalArgumentException("Inserted a received payment without having an invoice") @@ -378,18 +378,18 @@ class PaymentsDbSpec extends AnyFunSuite { postCheck = _ => { val db = dbs.db.payments - assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) - assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2, pr1)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) - - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1, ps3)) - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1)) - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) + assert(db.getIncomingPayment(i1.paymentHash) == Some(pr1)) + assert(db.getIncomingPayment(i2.paymentHash) == Some(pr2)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2, pr1)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) + + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1, ps3)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) } ) } @@ -403,22 +403,22 @@ class PaymentsDbSpec extends AnyFunSuite { assert(!db.receiveIncomingPayment(unknownPaymentHash, 12345678 msat)) assert(db.getIncomingPayment(unknownPaymentHash).isEmpty) - val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) - val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) - val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) - val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val expiredInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) - val pendingInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #3"), CltvExpiryDelta(18)) - val pendingInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #4"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) - val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) + val pendingInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #3"), CltvExpiryDelta(18)) + val pendingInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #4"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) + val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #5"), CltvExpiryDelta(18)) - val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #6"), CltvExpiryDelta(18), expirySeconds = Some(60)) + val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #5"), CltvExpiryDelta(18)) + val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #6"), CltvExpiryDelta(18), expirySeconds = Some(60)) val receivedAt1 = TimestampMilli.now() + 1.milli val receivedAt2 = TimestampMilli.now() + 2.milli - val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(561 msat, receivedAt2)) - val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) + val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(561 msat, receivedAt2)) + val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice1, pendingPayment1.paymentPreimage) db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage, PaymentType.SwapIn) @@ -427,33 +427,33 @@ class PaymentsDbSpec extends AnyFunSuite { db.addIncomingPayment(paidInvoice1, payment1.paymentPreimage) db.addIncomingPayment(paidInvoice2, payment2.paymentPreimage) - assert(db.getIncomingPayment(pendingInvoice1.paymentHash) === Some(pendingPayment1)) - assert(db.getIncomingPayment(expiredInvoice2.paymentHash) === Some(expiredPayment2)) - assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1.copy(status = IncomingPaymentStatus.Pending))) + assert(db.getIncomingPayment(pendingInvoice1.paymentHash) == Some(pendingPayment1)) + assert(db.getIncomingPayment(expiredInvoice2.paymentHash) == Some(expiredPayment2)) + assert(db.getIncomingPayment(paidInvoice1.paymentHash) == Some(payment1.copy(status = IncomingPaymentStatus.Pending))) val now = TimestampMilli.now() - assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) - assert(db.listExpiredIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) === Nil) - assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listExpiredIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) == Nil) + assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) db.receiveIncomingPayment(paidInvoice1.paymentHash, 461 msat, receivedAt1) db.receiveIncomingPayment(paidInvoice1.paymentHash, 100 msat, receivedAt2) // adding another payment to this invoice should sum db.receiveIncomingPayment(paidInvoice2.paymentHash, 1111 msat, receivedAt2) - assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1)) + assert(db.getIncomingPayment(paidInvoice1.paymentHash) == Some(payment1)) - assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listIncomingPayments(now - 60.seconds, now) === Seq(pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) === Seq(payment1, payment2)) + assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listIncomingPayments(now - 60.seconds, now) == Seq(pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment1, payment2)) assert(db.removeIncomingPayment(paidInvoice1.paymentHash).isFailure) - db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage === "Cannot remove a received incoming payment")) - assert(db.removeIncomingPayment(pendingPayment1.paymentRequest.paymentHash).isSuccess) - assert(db.removeIncomingPayment(pendingPayment1.paymentRequest.paymentHash).isSuccess) // idempotent - assert(db.removeIncomingPayment(expiredPayment1.paymentRequest.paymentHash).isSuccess) - assert(db.removeIncomingPayment(expiredPayment1.paymentRequest.paymentHash).isSuccess) // idempotent + db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage == "Cannot remove a received incoming payment")) + assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) + assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) // idempotent + assert(db.removeIncomingPayment(expiredPayment1.invoice.paymentHash).isSuccess) + assert(db.removeIncomingPayment(expiredPayment1.invoice.paymentHash).isSuccess) // idempotent } } @@ -462,7 +462,7 @@ class PaymentsDbSpec extends AnyFunSuite { val db = dbs.payments val parentId = UUID.randomUUID() - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100 unixms, Some(i1), OutgoingPaymentStatus.Pending) val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, PaymentType.SwapOut, 456 msat, 600 msat, dave, 200 unixms, None, OutgoingPaymentStatus.Pending) @@ -476,12 +476,12 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.listOutgoingPayments(1 unixms, 300 unixms).toList == Seq(s1, s2)) assert(db.listOutgoingPayments(1 unixms, 150 unixms).toList == Seq(s1)) assert(db.listOutgoingPayments(150 unixms, 250 unixms).toList == Seq(s2)) - assert(db.getOutgoingPayment(s1.id) === Some(s1)) - assert(db.getOutgoingPayment(UUID.randomUUID()) === None) - assert(db.listOutgoingPayments(s2.paymentHash) === Seq(s1, s2)) - assert(db.listOutgoingPayments(s1.id) === Nil) - assert(db.listOutgoingPayments(parentId) === Seq(s1, s2)) - assert(db.listOutgoingPayments(ByteVector32.Zeroes) === Nil) + assert(db.getOutgoingPayment(s1.id) == Some(s1)) + assert(db.getOutgoingPayment(UUID.randomUUID()) == None) + assert(db.listOutgoingPayments(s2.paymentHash) == Seq(s1, s2)) + assert(db.listOutgoingPayments(s1.id) == Nil) + assert(db.listOutgoingPayments(parentId) == Seq(s1, s2)) + assert(db.listOutgoingPayments(ByteVector32.Zeroes) == Nil) val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300 unixms) val s4 = s2.copy(id = UUID.randomUUID(), paymentType = PaymentType.Standard, createdAt = 301 unixms) @@ -490,10 +490,10 @@ class PaymentsDbSpec extends AnyFunSuite { db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms)) val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms)) - assert(db.getOutgoingPayment(s3.id) === Some(ss3)) + assert(db.getOutgoingPayment(s3.id) == Some(ss3)) db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320 unixms)) val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42)))), Some(alice)), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), Some(carol))), 320 unixms)) - assert(db.getOutgoingPayment(s4.id) === Some(ss4)) + assert(db.getOutgoingPayment(s4.id) == Some(ss4)) // can't update again once it's in a final state assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None))))) @@ -505,9 +505,9 @@ class PaymentsDbSpec extends AnyFunSuite { val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) db.updateOutgoingPayment(paymentSent) - assert(db.getOutgoingPayment(s1.id) === Some(ss1)) - assert(db.getOutgoingPayment(s2.id) === Some(ss2)) - assert(db.listOutgoingPayments(parentId) === Seq(ss1, ss2, ss3, ss4)) + assert(db.getOutgoingPayment(s1.id) == Some(ss1)) + assert(db.getOutgoingPayment(s2.id) == Some(ss2)) + assert(db.listOutgoingPayments(parentId) == Seq(ss1, ss2, ss3, ss4)) // can't update again once it's in a final state assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentFailed(s1.id, s1.paymentHash, Nil))) @@ -518,16 +518,16 @@ class PaymentsDbSpec extends AnyFunSuite { val db = new SqlitePaymentsDb(TestDatabases.sqliteInMemory()) // -- feed db with incoming payments - val expiredInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32(), PaymentType.Standard, 100 unixms, IncomingPaymentStatus.Expired) - val pendingInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32(), alicePriv, Left("incoming #2"), CltvExpiryDelta(18)) + val pendingInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32(), alicePriv, Left("incoming #2"), CltvExpiryDelta(18)) val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32(), PaymentType.Standard, 120 unixms, IncomingPaymentStatus.Pending) - val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32(), alicePriv, Left("incoming #3"), CltvExpiryDelta(18)) + val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32(), alicePriv, Left("incoming #3"), CltvExpiryDelta(18)) val receivedAt1 = 150 unixms val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, 130 unixms, IncomingPaymentStatus.Received(561 msat, receivedAt1)) - val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32(), alicePriv, Left("incoming #4"), CltvExpiryDelta(18)) + val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32(), alicePriv, Left("incoming #4"), CltvExpiryDelta(18)) val receivedAt2 = 160 unixms - val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(889 msat, receivedAt2)) + val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(889 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice, pendingPayment.paymentPreimage) db.addIncomingPayment(expiredInvoice, expiredPayment.paymentPreimage) db.addIncomingPayment(paidInvoice1, receivedPayment1.paymentPreimage) @@ -538,7 +538,7 @@ class PaymentsDbSpec extends AnyFunSuite { // -- feed db with outgoing payments val parentId1 = UUID.randomUUID() val parentId2 = UUID.randomUUID() - val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) // 1st attempt, pending -> failed val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 200 unixms, Some(invoice), OutgoingPaymentStatus.Pending) @@ -593,7 +593,7 @@ class PaymentsDbSpec extends AnyFunSuite { object PaymentsDbSpec { val (alicePriv, bobPriv, carolPriv, davePriv) = (randomKey(), randomKey(), randomKey(), randomKey()) val (alice, bob, carol, dave) = (alicePriv.publicKey, bobPriv.publicKey, carolPriv.publicKey, davePriv.publicKey) - val hop_ab = ChannelHop(alice, bob, ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) + val hop_ab = ChannelHop(ShortChannelId(42), alice, bob, ChannelRelayParams.FromAnnouncement(ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None))) val hop_bc = NodeHop(bob, carol, CltvExpiryDelta(14), 1 msat) val (preimage1, preimage2, preimage3, preimage4) = (randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) val (paymentHash1, paymentHash2, paymentHash3, paymentHash4) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3), Crypto.sha256(preimage4)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index b1865e3431..848b946f04 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.db.pg.PgPeersDb import fr.acinq.eclair.db.sqlite.SqlitePeersDb @@ -56,20 +56,20 @@ class PeersDbSpec extends AnyFunSuite { val peer_2 = TestCase(randomKey().publicKey, Tor2("z4zif3fy7fe7bpg3", 4231)) val peer_3 = TestCase(randomKey().publicKey, Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)) - assert(db.listPeers().toSet === Set.empty) + assert(db.listPeers().toSet == Set.empty) db.addOrUpdatePeer(peer_1.nodeId, peer_1.nodeAddress) - assert(db.getPeer(peer_1.nodeId) === Some(peer_1.nodeAddress)) - assert(db.getPeer(peer_2.nodeId) === None) + assert(db.getPeer(peer_1.nodeId) == Some(peer_1.nodeAddress)) + assert(db.getPeer(peer_2.nodeId) == None) db.addOrUpdatePeer(peer_1.nodeId, peer_1.nodeAddress) // duplicate is ignored - assert(db.listPeers().size === 1) + assert(db.listPeers().size == 1) db.addOrUpdatePeer(peer_2.nodeId, peer_2.nodeAddress) db.addOrUpdatePeer(peer_3.nodeId, peer_3.nodeAddress) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1, peer_2, peer_3)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1, peer_2, peer_3)) db.removePeer(peer_2.nodeId) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1, peer_3)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1, peer_3)) db.addOrUpdatePeer(peer_1_bis.nodeId, peer_1_bis.nodeAddress) - assert(db.getPeer(peer_1.nodeId) === Some(peer_1_bis.nodeAddress)) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1_bis, peer_3)) + assert(db.getPeer(peer_1.nodeId) == Some(peer_1_bis.nodeAddress)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1_bis, peer_3)) } } @@ -93,17 +93,17 @@ class PeersDbSpec extends AnyFunSuite { val a = randomKey().publicKey val b = randomKey().publicKey - assert(db.getRelayFees(a) === None) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == None) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(a, RelayFees(1 msat, 123)) - assert(db.getRelayFees(a) === Some(RelayFees(1 msat, 123))) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == Some(RelayFees(1 msat, 123))) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(a, RelayFees(2 msat, 456)) - assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(b, RelayFees(3 msat, 789)) - assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getRelayFees(b) === Some(RelayFees(3 msat, 789))) + assert(db.getRelayFees(a) == Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) == Some(RelayFees(3 msat, 789))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index b0b7d4530f..d2ec57a89f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, HtlcSettlementCommand} import fr.acinq.eclair.db.pg.PgPendingCommandsDb @@ -59,7 +59,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { val msg3 = CMD_FAIL_HTLC(3, Left(randomBytes32())) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) - assert(db.listSettlementCommands(channelId1).toSet === Set.empty) + assert(db.listSettlementCommands(channelId1).toSet == Set.empty) db.addSettlementCommand(channelId1, msg0) db.addSettlementCommand(channelId1, msg0) // duplicate db.addSettlementCommand(channelId1, msg1) @@ -68,11 +68,11 @@ class PendingCommandsDbSpec extends AnyFunSuite { db.addSettlementCommand(channelId1, msg4) db.addSettlementCommand(channelId2, msg0) // same messages but for different channel db.addSettlementCommand(channelId2, msg1) - assert(db.listSettlementCommands(channelId1).toSet === Set(msg0, msg1, msg2, msg3, msg4)) - assert(db.listSettlementCommands(channelId2).toSet === Set(msg0, msg1)) - assert(db.listSettlementCommands().toSet === Set((channelId1, msg0), (channelId1, msg1), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) + assert(db.listSettlementCommands(channelId1).toSet == Set(msg0, msg1, msg2, msg3, msg4)) + assert(db.listSettlementCommands(channelId2).toSet == Set(msg0, msg1)) + assert(db.listSettlementCommands().toSet == Set((channelId1, msg0), (channelId1, msg1), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) db.removeSettlementCommand(channelId1, msg1.id) - assert(db.listSettlementCommands().toSet === Set((channelId1, msg0), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) + assert(db.listSettlementCommands().toSet == Set((channelId1, msg0), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) } } @@ -98,7 +98,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { dbName = PgPendingCommandsDb.DB_NAME, targetVersion = PgPendingCommandsDb.CURRENT_VERSION, postCheck = _ => - assert(dbs.pendingCommands.listSettlementCommands().toSet === testCases.map(tc => tc.channelId -> tc.cmd)) + assert(dbs.pendingCommands.listSettlementCommands().toSet == testCases.map(tc => tc.channelId -> tc.cmd)) ) case dbs: TestSqliteDatabases => migrationCheck( @@ -120,7 +120,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { dbName = SqlitePendingCommandsDb.DB_NAME, targetVersion = SqlitePendingCommandsDb.CURRENT_VERSION, postCheck = _ => - assert(dbs.pendingCommands.listSettlementCommands().toSet === testCases.map(tc => tc.channelId -> tc.cmd)) + assert(dbs.pendingCommands.listSettlementCommands().toSet == testCases.map(tc => tc.channelId -> tc.cmd)) ) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index ff90d0ea41..a7f8b7f39b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -1,11 +1,12 @@ package fr.acinq.eclair.db import com.opentable.db.postgres.embedded.EmbeddedPostgres -import com.typesafe.config.{Config, ConfigFactory, ConfigValue} +import com.typesafe.config.{Config, ConfigFactory} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.PgLock.{LeaseLock, LockFailure, LockFailureHandler} -import fr.acinq.eclair.db.pg.PgUtils.{JdbcUrlChanged, migrateTable, using} +import fr.acinq.eclair.db.pg.PgUtils.{migrateTable, using} import fr.acinq.eclair.payment.ChannelPaymentRelayed import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec @@ -31,13 +32,13 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() val instanceId1 = UUID.randomUUID() // this will lock the database for this instance id - val db1 = Databases.postgres(config, instanceId1, datadir, LockFailureHandler.logAndThrow) + val db1 = Databases.postgres(config, instanceId1, datadir, None, LockFailureHandler.logAndThrow) assert( intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) + }.lockFailure == LockFailure.AlreadyLocked(instanceId1)) // we can renew the lease at will db1.obtainExclusiveLock() @@ -47,8 +48,8 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually assert( intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) + }.lockFailure == LockFailure.AlreadyLocked(instanceId1)) // we close the first connection db1.dataSource.close() @@ -58,7 +59,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // now we can put a lock with a different instance id val instanceId2 = UUID.randomUUID() - val db2 = Databases.postgres(config, instanceId2, datadir, LockFailureHandler.logAndThrow) + val db2 = Databases.postgres(config, instanceId2, datadir, None, LockFailureHandler.logAndThrow) // we close the second connection db2.dataSource.close() @@ -67,8 +68,8 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // but we don't wait for the previous lease to expire, so we can't take over right now assert(intercept[LockFailureHandler.LockException] { // this will fail because even if we have acquired the table lock, the previous lease still hasn't expired - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId2)) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) + }.lockFailure == LockFailure.AlreadyLocked(instanceId2)) pg.close() } @@ -80,7 +81,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() val instanceId1 = UUID.randomUUID() // this will lock the database for this instance id - val db = Databases.postgres(config, instanceId1, datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, instanceId1, datadir, None, LockFailureHandler.logAndThrow) implicit val ds: DataSource = db.dataSource // dummy query works @@ -132,8 +133,9 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually val config = PgUtilsSpec.testConfig(pg.getPort) val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") datadir.mkdirs() + val jdbcUrlPath = new File(datadir, "last_jdbcurl") // this will lock the database for this instance id - val db = Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, UUID.randomUUID(), datadir, Some(jdbcUrlPath), LockFailureHandler.logAndThrow) // we close the first connection db.dataSource.close() @@ -142,7 +144,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // here we change the config to simulate an involuntary change in the server we connect to val config1 = ConfigFactory.parseString("postgres.port=1234").withFallback(config) intercept[JdbcUrlChanged] { - Databases.postgres(config1, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config1, UUID.randomUUID(), datadir, Some(jdbcUrlPath), LockFailureHandler.logAndThrow) } pg.close() @@ -155,7 +157,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually .withFallback(PgUtilsSpec.testConfig(pg.getPort)) val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") datadir.mkdirs() - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) } test("safety checks") { @@ -165,7 +167,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() { - val db = Databases.postgres(baseConfig, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(baseConfig, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) db.channels.updateChannelMeta(ChannelCodecsSpec.normal.channelId, ChannelEvent.EventType.Created) db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-A", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 45.days)) @@ -195,7 +197,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually | } |}""".stripMargin) val config = safetyConfig.withFallback(baseConfig) - val db = Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) db.dataSource.close() } @@ -220,7 +222,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually |}""".stripMargin) val config = safetyConfig.withFallback(baseConfig) intercept[IllegalArgumentException] { - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) } } @@ -259,7 +261,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually using(pg.getPostgresDatabase.getConnection.createStatement()) { statement => val rs = statement.executeQuery("SELECT bar FROM foo") - assert(rs.map(_.getInt("bar")).toSet === Set(10, 20, 30)) + assert(rs.map(_.getInt("bar")).toSet == Set(10, 20, 30)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala deleted file mode 100644 index e280b622c4..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.db - -import fr.acinq.bitcoin.SatoshiLong -import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} -import fr.acinq.eclair.db.pg.PgUtils.setVersion -import fr.acinq.eclair.db.sqlite.SqliteFeeratesDb -import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import org.scalatest.funsuite.AnyFunSuite - -class SqliteFeeratesDbSpec extends AnyFunSuite { - - val feerate: FeeratesPerKB = FeeratesPerKB( - mempoolMinFee = FeeratePerKB(10000 sat), - block_1 = FeeratePerKB(150000 sat), - blocks_2 = FeeratePerKB(120000 sat), - blocks_6 = FeeratePerKB(100000 sat), - blocks_12 = FeeratePerKB(90000 sat), - blocks_36 = FeeratePerKB(70000 sat), - blocks_72 = FeeratePerKB(50000 sat), - blocks_144 = FeeratePerKB(20000 sat), - blocks_1008 = FeeratePerKB(10000 sat)) - - test("init database 2 times in a row") { - val sqlite = TestDatabases.sqliteInMemory() - val db1 = new SqliteFeeratesDb(sqlite) - val db2 = new SqliteFeeratesDb(sqlite) - } - - test("add/get feerates") { - val sqlite = TestDatabases.sqliteInMemory() - val db = new SqliteFeeratesDb(sqlite) - - db.addOrUpdateFeerates(feerate) - assert(db.getFeerates().get == feerate) - } - - test("migration v1 -> current") { - val sqlite = TestDatabases.sqliteInMemory() - - using(sqlite.createStatement()) { statement => - statement.executeUpdate( - """ - |CREATE TABLE IF NOT EXISTS feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - setVersion(statement, "feerates", 1) - } - - using(sqlite.createStatement()) { statement => - assert(getVersion(statement, "feerates").contains(1)) - } - - // Version 1 was missing the 1008 block target. - using(sqlite.prepareStatement("INSERT INTO feerates_per_kb VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setLong(1, feerate.block_1.toLong) - statement.setLong(2, feerate.blocks_2.toLong) - statement.setLong(3, feerate.blocks_6.toLong) - statement.setLong(4, feerate.blocks_12.toLong) - statement.setLong(5, feerate.blocks_36.toLong) - statement.setLong(6, feerate.blocks_72.toLong) - statement.setLong(7, feerate.blocks_144.toLong) - statement.setLong(8, TimestampMilli.now().toLong) - statement.executeUpdate() - } - - val migratedDb = new SqliteFeeratesDb(sqlite) - using(sqlite.createStatement()) { statement => - assert(getVersion(statement, "feerates").contains(SqliteFeeratesDb.CURRENT_VERSION)) - } - - // When migrating, we simply copy the estimate for blocks 144 to blocks 1008. - assert(migratedDb.getFeerates() === Some(feerate.copy(blocks_1008 = feerate.blocks_144, mempoolMinFee = feerate.blocks_144))) - migratedDb.addOrUpdateFeerates(feerate) - assert(migratedDb.getFeerates() === Some(feerate)) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index 03082bb10b..1f3ec53e4d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -16,11 +16,16 @@ package fr.acinq.eclair.db -import java.sql.SQLException -import fr.acinq.eclair.{TestConstants, TestDatabases} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.sqlite.SqliteUtils.using +import fr.acinq.eclair.{TestDatabases, TestUtils} import org.scalatest.funsuite.AnyFunSuite +import java.io.File +import java.nio.file.Files +import java.sql.SQLException +import java.util.UUID + class SqliteUtilsSpec extends AnyFunSuite { test("using with auto-commit disabled") { @@ -35,9 +40,9 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(!results.next()) } @@ -49,9 +54,9 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(!results.next()) } @@ -63,15 +68,39 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(results.next()) - assert(results.getLong("id") === 3) + assert(results.getLong("id") == 3) assert(results.next()) - assert(results.getLong("id") === 4) + assert(results.getLong("id") == 4) assert(!results.next()) } } + test("jdbc url check") { + val datadir = new File(TestUtils.BUILD_DIRECTORY, s"sqlite_test_${UUID.randomUUID()}") + datadir.mkdirs() + val jdbcUrlPath = new File(datadir, "last_jdbcurl") + + // first start : write to file + val db1 = Databases.sqlite(datadir, Some(jdbcUrlPath)) + db1.channels.sqlite.close() + + assert(Files.readString(jdbcUrlPath.toPath).trim == "sqlite") + + // 2nd start : no-op + val db2 = Databases.sqlite(datadir, Some(jdbcUrlPath)) + db2.channels.sqlite.close() + + // we modify the file + Files.writeString(jdbcUrlPath.toPath, "postgres") + + // boom + intercept[JdbcUrlChanged] { + Databases.sqlite(datadir, Some(jdbcUrlPath)) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index fa64e7d786..390f752b85 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef +import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.pattern.pipe import akka.testkit.TestProbe -import com.google.common.net.HostAndPort import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, ScriptFlags} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel._ @@ -35,7 +36,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, PermanentChannelFailure, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -68,11 +69,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { */ def scriptPubKeyToAddress(scriptPubKey: ByteVector): String = Script.parse(scriptPubKey) match { case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil => - Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) + Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash.toArray) case OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil => - Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash) - case OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil if pubKeyHash.length == 20 => Bech32.encodeWitnessAddress("bcrt", 0, pubKeyHash) - case OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil if scriptHash.length == 32 => Bech32.encodeWitnessAddress("bcrt", 0, scriptHash) + Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash.toArray) + case OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil if pubKeyHash.length == 20 => Bech32.encodeWitnessAddress("bcrt", 0, pubKeyHash.toArray) + case OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil if scriptHash.length == 32 => Bech32.encodeWitnessAddress("bcrt", 0, scriptHash.toArray) case _ => ??? } @@ -127,15 +128,25 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val sender = TestProbe() sender.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(minerAddress) = sender.expectMsgType[JValue] - // we create and announce a channel between C and F; we use push_msat to ensure both nodes have an output in the commit tx - connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) - generateBlocks(6, Some(minerAddress)) - awaitAnnouncements(2) // we subscribe to channel state transitions val stateListenerC = TestProbe() val stateListenerF = TestProbe() nodes("C").system.eventStream.subscribe(stateListenerC.ref, classOf[ChannelStateChanged]) nodes("F").system.eventStream.subscribe(stateListenerF.ref, classOf[ChannelStateChanged]) + // we create and announce a channel between C and F; we use push_msat to ensure both nodes have an output in the commit tx + connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) + awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) + generateBlocks(1, Some(minerAddress)) + // the funder sends its channel_ready after only one block + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_CHANNEL_READY, max = 30 seconds) + generateBlocks(2, Some(minerAddress)) + // the fundee sends its channel_ready after 3 blocks + awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + // we generate more blocks for the funding tx to be deeply buried and the channel to be announced + generateBlocks(3, Some(minerAddress)) + awaitAnnouncements(2) // first we make sure we are in sync with current blockchain height val currentBlockHeight = getBlockHeight() awaitCond(getBlockHeight() == currentBlockHeight, max = 20 seconds, interval = 1 second) @@ -145,20 +156,20 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPaymentToNode(100000000 msat, PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() + val paymentReq = SendPaymentToNode(paymentSender.ref, 100000000 msat, Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] // F gets the htlc val htlc = htlcReceiver.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds).add // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val dataC = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data - assert(dataC.commitments.commitmentFormat === commitmentFormat) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data + assert(dataC.commitments.commitmentFormat == commitmentFormat) val finalAddressC = scriptPubKeyToAddress(dataC.commitments.localParams.defaultFinalScriptPubKey) - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val dataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data - assert(dataF.commitments.commitmentFormat === commitmentFormat) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data + assert(dataF.commitments.commitmentFormat == commitmentFormat) val finalAddressF = scriptPubKeyToAddress(dataF.commitments.localParams.defaultFinalScriptPubKey) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) } @@ -172,14 +183,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F disconnectCF(htlc.channelId, sender) // we then have C unilaterally close the channel (which will make F redeem the htlc onchain) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we then wait for F to detect the unilateral close and go to CLOSING state awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc, which will make F redeem it on-chain - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) @@ -209,14 +220,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F disconnectCF(htlc.channelId, sender) // then we have F unilaterally close the channel - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) @@ -251,24 +262,24 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val Some(localCommit) = sender.expectMsgType[RES_GETSTATEDATA[DATA_CLOSING]].data.localCommitPublished + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val Some(localCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.localCommitPublished // we wait until the commit tx has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) waitForTxBroadcastOrConfirmed(localCommit.commitTx.txid, bitcoinClient, sender) // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we wait until the htlc-timeout has been broadcast - assert(localCommit.htlcTxs.size === 1) + assert(localCommit.htlcTxs.size == 1) waitForOutputSpent(localCommit.htlcTxs.keys.head, bitcoinClient, sender) // we generate more blocks for the htlc-timeout to reach enough confirmations generateBlocks(3, Some(minerAddress)) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](max = 60 seconds) assert(failed.id == paymentId) - assert(failed.paymentHash === htlc.paymentHash) + assert(failed.paymentHash == htlc.paymentHash) assert(failed.failures.nonEmpty) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) // C should have 2 recv transactions: its main output and the htlc timeout @@ -295,32 +306,32 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F to ensure the close can only be detected on-chain disconnectCF(htlc.channelId, sender) // we ask F to unilaterally close the channel - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we wait for C to detect the unilateral close awaitCond({ - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - sender.expectMsgType[RES_GETSTATEDATA[ChannelData]].data match { + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data match { case d: DATA_CLOSING if d.remoteCommitPublished.nonEmpty => true case _ => false } }, max = 30 seconds, interval = 1 second) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val Some(remoteCommit) = sender.expectMsgType[RES_GETSTATEDATA[DATA_CLOSING]].data.remoteCommitPublished + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val Some(remoteCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.remoteCommitPublished // we generate enough blocks to make the htlc timeout generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) // we wait until the claim-htlc-timeout has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - assert(remoteCommit.claimHtlcTxs.size === 1) + assert(remoteCommit.claimHtlcTxs.size == 1) waitForOutputSpent(remoteCommit.claimHtlcTxs.keys.head, bitcoinClient, sender) // and we generate blocks for the claim-htlc-timeout to reach enough confirmations generateBlocks(3, Some(minerAddress)) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](max = 60 seconds) assert(failed.id == paymentId) - assert(failed.paymentHash === htlc.paymentHash) + assert(failed.paymentHash == htlc.paymentHash) assert(failed.failures.nonEmpty) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) // C should have 2 recv transactions: its main output and the htlc timeout @@ -367,8 +378,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = { sender.send(paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams) + val invoice = sender.expectMsgType[Invoice] + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -391,17 +402,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMessage(1 second) - assert(commitmentsF.commitmentFormat === commitmentFormat) + assert(commitmentsF.commitmentFormat == commitmentFormat) // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable val localCommitF = commitmentsF.localCommit commitmentFormat match { - case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) + case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) } val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h } - assert(htlcTimeoutTxs.size === 2) - assert(htlcSuccessTxs.size === 2) + assert(htlcTimeoutTxs.size == 2) + assert(htlcSuccessTxs.size == 2) // we fulfill htlcs to get the preimages buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] buffer.forward(paymentHandlerF) @@ -422,8 +433,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate blocks to make htlcs timeout (nothing will happen in the channel because all of them have already been fulfilled) generateBlocks(40) // we retrieve C's default final address - sender.send(nodes("C").register, Register.Forward(sender.ref, commitmentsF.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey) // we prepare the revoked transactions F will publish val keyManagerF = nodes("F").nodeParams.channelKeyManager val channelKeyPathF = keyManagerF.keyPath(commitmentsF.localParams, commitmentsF.channelConfig) @@ -452,9 +463,9 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) } test("connect nodes") { @@ -491,61 +502,61 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(fundee.register, Symbol("channels")) val Some((_, fundeeChannel)) = sender.expectMsgType[Map[ByteVector32, ActorRef]].find(_._1 == tempChannelId) - sender.send(fundeeChannel, CMD_GETSTATEDATA(ActorRef.noSender)) - val channelId = sender.expectMsgType[RES_GETSTATEDATA[HasCommitments]].data.channelId + sender.send(fundeeChannel, CMD_GET_CHANNEL_DATA(ActorRef.noSender)) + val channelId = sender.expectMsgType[RES_GET_CHANNEL_DATA[PersistentChannelData]].data.channelId awaitCond({ - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_LOCKED + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY }) generateBlocks(6) // after 8 blocks the fundee is still waiting for more confirmations - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_CONFIRMED) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_CONFIRMED) // after 8 blocks the funder is still waiting for funding_locked from the fundee - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_LOCKED) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY) // simulate a disconnection sender.send(funder.switchboard, Peer.Disconnect(fundee.nodeParams.nodeId)) assert(sender.expectMsgType[String] == "disconnecting") awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[_]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[_]].state + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == OFFLINE && funderState == OFFLINE }) - // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send funding_locked + // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send channel_ready awaitCond({ // reconnection sender.send(fundee.switchboard, Peer.Connect( nodeId = funder.nodeParams.nodeId, - address_opt = Some(HostAndPort.fromParts(funder.nodeParams.publicAddresses.head.socketAddress.getHostString, funder.nodeParams.publicAddresses.head.socketAddress.getPort)), + address_opt = funder.nodeParams.publicAddresses.headOption, sender.ref, isPersistent = true )) sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](30 seconds) - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state - fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_FUNDING_LOCKED + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_CHANNEL_READY }, max = 30 seconds, interval = 10 seconds) // 5 extra blocks make it 13, just the amount of confirmations needed generateBlocks(5) awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == NORMAL && funderState == NORMAL }) @@ -555,14 +566,14 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { funder.system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // close that wumbo channel - sender.send(funder.register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val commitmentsC = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments + sender.send(funder.register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val commitmentsC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments val finalPubKeyScriptC = commitmentsC.localParams.defaultFinalScriptPubKey val fundingOutpoint = commitmentsC.commitInput.outPoint - sender.send(fundee.register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val finalPubKeyScriptF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey + sender.send(fundee.register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val finalPubKeyScriptF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey - fundee.register ! Register.Forward(sender.ref, channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] // we then wait for C and F to negotiate the closing fee awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) @@ -570,14 +581,14 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) - sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint.txid === fundingOutpoint.txid) + sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint.txid == fundingOutpoint.txid) }, max = 20 seconds, interval = 1 second) generateBlocks(3) awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSED, max = 60 seconds) bitcoinClient.lookForSpendingTx(None, fundingOutpoint.txid, fundingOutpoint.index.toInt).pipeTo(sender.ref) val closingTx = sender.expectMsgType[Transaction] - assert(closingTx.txOut.map(_.publicKeyScript).toSet === Set(finalPubKeyScriptC, finalPubKeyScriptF)) + assert(closingTx.txOut.map(_.publicKeyScript).toSet == Set(finalPubKeyScriptC, finalPubKeyScriptF)) awaitAnnouncements(1) } @@ -645,7 +656,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val stateEvent = eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds) if (stateEvent.currentState == NORMAL) { assert(stateEvent.commitments_opt.nonEmpty) - assert(stateEvent.commitments_opt.get.asInstanceOf[Commitments].channelType === expectedChannelType) + assert(stateEvent.commitments_opt.get.asInstanceOf[Commitments].channelType == expectedChannelType) count = count + 1 } } @@ -667,9 +678,9 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // retrieve the channelId of C <--> F val Some(channelId) = sender.expectMsgType[Map[ByteVector32, PublicKey]].find(_._2 == nodes("C").nodeParams.nodeId).map(_._1) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val initialStateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data - assert(initialStateDataF.commitments.channelType === expectedChannelType) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val initialStateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data + assert(initialStateDataF.commitments.channelType == expectedChannelType) val initialCommitmentIndex = initialStateDataF.commitments.localCommit.index // the 'to remote' address is a simple script spending to the remote payment basepoint with a 1-block CSV delay @@ -681,24 +692,24 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // let's make a payment to advance the commit index val amountMsat = 4200000.msat sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)) + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent](60 seconds) assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) // we make sure the htlc has been removed from F's commitment before we force-close awaitCond({ - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val stateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data stateDataF.commitments.localCommit.spec.htlcs.isEmpty }, max = 20 seconds, interval = 1 second) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val stateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data val commitmentIndex = stateDataF.commitments.localCommit.index val commitTx = stateDataF.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) @@ -716,7 +727,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // we kill the connection between C and F to ensure the close can only be detected on-chain disconnectCF(channelId, sender) // now let's force close the channel and check the toRemote is what we had at the beginning - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we then wait for C to detect the unilateral close and go to CLOSING state awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) @@ -734,7 +745,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteOutC.publicKeyScript)) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) - sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint === mainOutputC) + sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint == mainOutputC) }, max = 20 seconds, interval = 1 second) // get the claim-remote-output confirmed, then the channel can go to the CLOSED state @@ -779,9 +790,9 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) } test("connect nodes") { @@ -819,9 +830,9 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) } test("connect nodes") { @@ -829,7 +840,7 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { - testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 14a6e64132..1e383811e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -18,9 +18,8 @@ package fr.acinq.eclair.integration import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} -import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.channel._ @@ -81,14 +80,14 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit "eclair.bitcoind.zmqblock" -> s"tcp://127.0.0.1:$bitcoindZmqBlockPort", "eclair.bitcoind.zmqtx" -> s"tcp://127.0.0.1:$bitcoindZmqTxPort", "eclair.bitcoind.wallet" -> defaultWallet, - "eclair.mindepth-blocks" -> 2, - "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, - "eclair.max-block-processing-delay" -> "2 seconds", + "eclair.channel.mindepth-blocks" -> 2, + "eclair.channel.max-htlc-value-in-flight-msat" -> 100000000000L, + "eclair.channel.max-block-processing-delay" -> "2 seconds", + "eclair.channel.to-remote-delay-blocks" -> 24, + "eclair.channel.max-funding-satoshis" -> 500000000, "eclair.router.broadcast-interval" -> "2 seconds", "eclair.auto-reconnect" -> false, - "eclair.to-remote-delay-blocks" -> 24, - "eclair.multi-part-payment-expiry" -> "20 seconds", - "eclair.max-funding-satoshis" -> 500000000).asJava).withFallback(ConfigFactory.load()) + "eclair.multi-part-payment-expiry" -> "20 seconds").asJava).withFallback(ConfigFactory.load()) private val commonFeatures = ConfigFactory.parseMap(Map( s"eclair.features.${DataLossProtect.rfcName}" -> "optional", @@ -156,10 +155,9 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit def connect(node1: Kit, node2: Kit): Unit = { val sender = TestProbe() - val address = node2.nodeParams.publicAddresses.head sender.send(node1.switchboard, Peer.Connect( nodeId = node2.nodeParams.nodeId, - address_opt = Some(HostAndPort.fromParts(address.socketAddress.getHostString, address.socketAddress.getPort)), + address_opt = node2.nodeParams.publicAddresses.headOption, sender.ref, isPersistent = true )) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 6dd4603ec3..6c19262e89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -22,7 +22,9 @@ import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.bitcoin.Transaction +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.TestUtils.waitEventStreamSynced import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -85,7 +87,7 @@ class MessageIntegrationSpec extends IntegrationSpec { val res = probe.expectMsgType[SendOnionMessageResponse] assert(res.failureMessage.isEmpty) assert(res.response.nonEmpty) - assert(res.response.get.unknownTlvs("29") === hex"ab") + assert(res.response.get.unknownTlvs("29") == hex"ab") } test("reply timeout") { @@ -137,7 +139,7 @@ class MessageIntegrationSpec extends IntegrationSpec { assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) } test("relay with channels-only and missing channel") { @@ -230,7 +232,7 @@ class MessageIntegrationSpec extends IntegrationSpec { assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) } test("channel relay with no-relay") { @@ -265,10 +267,10 @@ class MessageIntegrationSpec extends IntegrationSpec { val probe = TestProbe() probe.send(nodes("B").register, Symbol("channels")) val channelsB = probe.expectMsgType[Map[ByteVector32, ActorRef]] - assert(channelsB.size === 3) + assert(channelsB.size == 3) probe.send(nodes("D").register, Symbol("channels")) val channelsD = probe.expectMsgType[Map[ByteVector32, ActorRef]] - assert(channelsD.size === 3) + assert(channelsD.size == 3) channelsB.foreach { case (channelId, channel) => if (!channelsD.contains(channelId)) { @@ -302,12 +304,13 @@ class MessageIntegrationSpec extends IntegrationSpec { val probe = TestProbe() val eventListener = TestProbe() nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage]) + waitEventStreamSynced(nodes("C").system.eventStream) alice.sendOnionMessage(nodes("B").nodeParams.nodeId :: Nil, Left(nodes("C").nodeParams.nodeId), None, hex"7300").pipeTo(probe.ref) assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.pathId === None) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(115), hex""))) + assert(r.pathId.isEmpty) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(115), hex""))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala new file mode 100644 index 0000000000..76a76aad40 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala @@ -0,0 +1,127 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.integration + +import akka.pattern.pipe +import akka.testkit.TestProbe +import akka.util.Timeout +import com.typesafe.config.ConfigFactory +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import fr.acinq.bitcoin.scalacompat.SatoshiLong +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} +import fr.acinq.eclair.channel.{ChannelStateChanged, NORMAL} +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} +import fr.acinq.eclair.db.OutgoingPaymentStatus.Succeeded +import fr.acinq.eclair.message.OnionMessages +import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentEvent} +import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived +import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.perHopPayloadCodec +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer +import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.Invoice +import fr.acinq.eclair.wire.protocol.TlvStream +import fr.acinq.eclair.{EclairImpl, Features, MilliSatoshiLong, PayOfferResponse, randomBytes32} +import scodec.bits.ByteVector + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class OffersIntegrationSpec extends IntegrationSpec { + implicit val timeout: Timeout = FiniteDuration(30, SECONDS) + + test("setup eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 30700, "eclair.api.port" -> 30780, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonConfig)) + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 30701, "eclair.api.port" -> 30781, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonConfig)) + + val sender = TestProbe() + val eventListener = TestProbe() + nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) + + connect(nodes("B"), nodes("A"), 1000000 sat, 0 msat) + + val numberOfChannels = 1 + val channelEndpointsCount = 2 * numberOfChannels + + // we make sure all channels have set up their WatchConfirmed for the funding tx + awaitCond({ + val watches = nodes.values.foldLeft(Set.empty[Watch[_]]) { + case (watches, setup) => + setup.watcher ! ZmqWatcher.ListWatches(sender.ref) + watches ++ sender.expectMsgType[Set[Watch[_]]] + } + watches.count(_.isInstanceOf[WatchFundingConfirmed]) == channelEndpointsCount + }, max = 20 seconds, interval = 1 second) + + // confirming the funding tx + generateBlocks(2) + + within(60 seconds) { + var count = 0 + while (count < channelEndpointsCount) { + if (eventListener.expectMsgType[ChannelStateChanged](60 seconds).currentState == NORMAL) count = count + 1 + } + } + } + + test("simple bolt11 payment") { + val alice = new EclairImpl(nodes("A")) + val bob = new EclairImpl(nodes("B")) + + val preimage = randomBytes32() + val invoice = Await.result(alice.receive(Left("simple bolt11 invoice"), Some(100000 msat), None, None, Some(preimage)), 10 seconds) + + bob.send(None,100000 msat, invoice) + + Thread.sleep(2000) + + val probe = TestProbe() + bob.sentInfo(Right(invoice.paymentHash)).pipeTo(probe.ref) + + val response = probe.expectMsgType[Seq[OutgoingPayment]](1 minute) + val OutgoingPaymentStatus.Succeeded(preimageReceived, _, _, _) = response.head.status + assert(preimageReceived === preimage) + } + + test("simple offer") { + val alice = new EclairImpl(nodes("A")) + val bob = new EclairImpl(nodes("B")) + + val eventListener = TestProbe() + nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage]) + + val offer = Offer(Some(100000 msat), "test offer", nodes("A").nodeParams.nodeId, Features.empty, nodes("A").nodeParams.chainHash) + + val probe = TestProbe() + bob.payOffer(offer, 100000 msat, 1).pipeTo(probe.ref) + + val preimage = randomBytes32() + val dummyInvoice = Await.result(alice.receive(Left("dummy bolt11 invoice"), Some(100000 msat), None, None, Some(preimage)), 10 seconds) + + val invoiceRequestPayload = eventListener.expectMsgType[OnionMessages.ReceiveMessage].finalPayload + val invoiceRequest = invoiceRequestPayload.invoiceRequest.get.request + val invoiceRequestReplyPath = invoiceRequestPayload.replyPath.get.blindedRoute + val invoice = Bolt12Invoice(offer, invoiceRequest, preimage, nodes("A").nodeParams.privateKey, dummyInvoice.minFinalCltvExpiryDelta, Features.empty, randomBytes32()) + val encodedInvoice = perHopPayloadCodec.encode(TlvStream(Invoice(invoice))).require.bytes + alice.sendOnionMessage(Nil, Right(invoiceRequestReplyPath), None, encodedInvoice) + + val response = probe.expectMsgType[PayOfferResponse](1 minute) + println(response) + assert(response.preimage contains preimage.toHex) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 61e0316126..017a2c7bd9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -17,22 +17,21 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef -import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer @@ -41,8 +40,8 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTra import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, NodeAnnouncement} -import fr.acinq.eclair.{CltvExpiryDelta, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -58,13 +57,13 @@ import scala.jdk.CollectionConverters._ class PaymentIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private - instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) - instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.channel.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.channel.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.channel.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.channel.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) } test("connect nodes") { @@ -73,6 +72,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // A---B ------- C ==== D // \ / \ // '--E--' '--F + // + // All channels have fees 1 sat + 200 millionths, except for G that have fees 1010 msat + 102 millionths val sender = TestProbe() val eventListener = TestProbe() @@ -112,22 +113,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { } } - def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int): Unit = { + def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, privateChannels: Int, publicChannels: Int, privateUpdates: Int, publicUpdates: Int): Unit = { val sender = TestProbe() subset.foreach { - case (_, setup) => - awaitCond({ - sender.send(setup.router, Router.GetNodes) - sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, Router.GetChannels) - sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, Router.GetChannelUpdates) - sender.expectMsgType[Iterable[ChannelUpdate]].size == updates - }, max = 60 seconds, interval = 1 second) + case (node, setup) => + withClue(node) { + awaitAssert({ + sender.send(setup.router, Router.GetRouterData) + val data = sender.expectMsgType[Router.Data] + assert(data.nodes.size == nodes) + assert(data.privateChannels.size == privateChannels) + assert(data.channels.size == publicChannels) + assert(data.privateChannels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == privateUpdates) + assert(data.channels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == publicUpdates) + }, max = 10 seconds, interval = 1 second) + } } } @@ -137,8 +137,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // A requires private channels, as a consequence: // - only A and B know about channel A-B (and there is no channel_announcement) // - A is not announced (no node_announcement) - awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, 6, 8, 18) - awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, 6, 8, 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, nodes = 6, privateChannels = 1, publicChannels = 8, privateUpdates = 2, publicUpdates = 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, nodes = 6, privateChannels = 0, publicChannels = 8, privateUpdates = 0, publicUpdates = 16) } test("wait for channels balance") { @@ -149,7 +149,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val routingState = sender.expectMsgType[Router.RoutingState] val publicChannels = routingState.channels.filter(pc => Set(pc.ann.nodeId1, pc.ann.nodeId2).contains(nodeId)) assert(publicChannels.nonEmpty) - publicChannels.foreach(pc => assert(pc.meta_opt.map(m => m.balance1 > 0.msat || m.balance2 > 0.msat) === Some(true), pc)) + publicChannels.foreach(pc => assert(pc.meta_opt.exists(m => m.balance1 > 0.msat || m.balance2 > 0.msat), pc)) } test("send an HTLC A->D") { @@ -159,16 +159,16 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.paymentMetadata.nonEmpty) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.paymentMetadata.nonEmpty) // then we make the actual payment - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) } test("send an HTLC A->D with an invalid expiry delta for B") { @@ -178,24 +178,24 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").router, Router.GetChannels) val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)).get.shortChannelId // we also need the full commitment - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) - val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + val normalBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL] // we then forge a new channel_update for B-C... - val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDelta + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) + val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer - nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) + nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, normalBC.channelId, normalBC.shortIds, normalBC.commitments.remoteParams.nodeId, normalBC.channelAnnouncement, channelUpdateBC, normalBC.commitments)) // we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) def updateFor(n: PublicKey, pc: PublicChannel): Option[ChannelUpdate] = if (n == pc.ann.nodeId1) pc.update_1_opt else if (n == pc.ann.nodeId2) pc.update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") @@ -209,17 +209,17 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former sender.expectNoMessage(3 seconds) - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, BroadcastChannelUpdate(PeriodicRefresh)) - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) - val channelUpdateBC_new = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].channelUpdate + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, BroadcastChannelUpdate(PeriodicRefresh)) + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + val channelUpdateBC_new = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].channelUpdate logger.info(s"channelUpdateBC=$channelUpdateBC") logger.info(s"channelUpdateBC_new=$channelUpdateBC_new") assert(channelUpdateBC_new.timestamp > channelUpdateBC.timestamp) - assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.expiryDelta) + assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.channelConf.expiryDelta) awaitCond({ sender.send(nodes("A").router, Router.GetChannelsMap) val u = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]].apply(channelUpdateBC.shortChannelId)).get - u.cltvExpiryDelta == nodes("B").nodeParams.expiryDelta + u.cltvExpiryDelta == nodes("B").nodeParams.channelConf.expiryDelta }, max = 30 seconds, interval = 1 second) } @@ -228,9 +228,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D val amountMsat = 300000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[UUID] @@ -240,17 +240,17 @@ class PaymentIntegrationSpec extends IntegrationSpec { test("send an HTLC A->D with an unknown payment hash") { val sender = TestProbe() val amount = 100000000 msat - val unknownInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) - val pr = SendPaymentToNode(amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) - sender.send(nodes("A").paymentInitiator, pr) + val unknownInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) + val invoice = SendPaymentToNode(sender.ref, amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + sender.send(nodes("A").paymentInitiator, invoice) // A will receive an error from D and won't retry val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) } test("send an HTLC A->D with a lower amount than requested") { @@ -258,19 +258,19 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of only 1 mBTC - val sendReq = SendPaymentToNode(100000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 100000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight()))) } test("send an HTLC A->D with too much overpayment") { @@ -278,19 +278,19 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of 6 mBTC - val sendReq = SendPaymentToNode(600000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 600000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(paymentId == failed.id) - assert(failed.paymentHash === pr.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight()))) } test("send an HTLC A->D with a reasonable overpayment") { @@ -298,10 +298,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPaymentToNode(300000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 300000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -312,9 +312,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { for (_ <- 0 until 7) { val amountMsat = 1000000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 payment"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed @@ -326,10 +326,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from C val amountMsat = 2000.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amountMsat), Left("Change from coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) @@ -340,36 +340,36 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 1000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("split the restaurant bill"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 5, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) - assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) + assert(paymentSent.recipientNodeId == nodes("D").nodeParams.nodeId, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) assert(paymentSent.feesPaid > 0.msat, paymentSent) assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent) assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent) val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) assert(paymentParts.length == paymentSent.parts.length, paymentParts) - assert(paymentParts.map(_.amount).sum === amount, paymentParts) + assert(paymentParts.map(_.amount).sum == amount, paymentParts) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts) awaitCond(nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()) - assert(sent.length === 1, sent) - assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) + assert(sent.length == 1, sent) + assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) - awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(receivedAmount === amount) + awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(receivedAmount == amount) } // NB: this test may take 20 seconds to complete (multi-part payment timeout). @@ -379,21 +379,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { // (the link C->D has too much capacity on D's side). val amount = 2000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("well that's an expensive restaurant bill"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend > amount) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) assert(paymentFailed.failures.length > 1, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val canSend2 = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum @@ -406,27 +406,27 @@ class PaymentIntegrationSpec extends IntegrationSpec { // This amount is greater than any channel capacity between D and C, so it should be split. val amount = 5100000000L.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), Left("lemme borrow some money"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 3, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 0.msat, paymentSent) // no fees when using direct channels val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) - assert(paymentParts.map(_.amount).sum === amount, paymentParts) + assert(paymentParts.map(_.amount).sum == amount, paymentParts) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts) - awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(receivedAmount === amount) + awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(receivedAmount == amount) } test("send a multi-part payment D->C greater than balance D->C (temporary local failure)") { @@ -434,21 +434,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { // This amount is greater than the current capacity between D and C. val amount = 10000000000L.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), Left("lemme borrow more money"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels()) val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend < amount) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) - val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming) + val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(incoming.get.status == IncomingPaymentStatus.Pending, incoming) sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels()) val canSend2 = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum @@ -461,41 +461,43 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 4000000000L.msat sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amount), Left("like trampoline much?"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) + + // The best route from G is G -> C -> F which has a fee of 1210091 msat // The first attempt should fail, but the second one should succeed. - val attempts = (1000 msat, CltvExpiryDelta(42)) :: (1000000 msat, CltvExpiryDelta(288)) :: Nil - val payment = SendTrampolinePayment(amount, pr, nodes("G").nodeParams.nodeId, attempts, routeParams = integrationTestRouteParams) + val attempts = (1210000 msat, CltvExpiryDelta(42)) :: (1210100 msat, CltvExpiryDelta(288)) :: Nil + val payment = SendTrampolinePayment(amount, invoice, nodes("G").nodeParams.nodeId, attempts, routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) - assert(paymentSent.recipientNodeId === nodes("F").nodeParams.nodeId, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 1000000.msat, paymentSent) - assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientNodeId == nodes("F").nodeParams.nodeId, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 1210100.msat, paymentSent) + assert(paymentSent.nonTrampolineFees == 0.msat, paymentSent) - awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(receivedAmount === amount) + awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(receivedAmount == amount) awaitCond({ - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 1210100.msat, relayed) val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("F").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("F").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 1210100.msat, outgoingSuccess) } test("send a trampoline payment D->B (via trampoline C)") { @@ -504,45 +506,49 @@ class PaymentIntegrationSpec extends IntegrationSpec { nodes("B").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) val amount = 2500000000L.msat sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) - assert(pr.paymentMetadata.nonEmpty) - - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) + assert(invoice.paymentMetadata.nonEmpty) + + // The direct route C -> B does not have enough capacity, the payment will be split between + // C -> B which would have a fee of 501000 if it could route the whole payment + // C -> G -> B which would have a fee of 757061 if it was used to route the whole payment + // The actual fee needed will be between these two values. + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((750000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("D").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 350000.msat, paymentSent) - assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 750000.msat, paymentSent) + assert(paymentSent.nonTrampolineFees == 0.msat, paymentSent) - awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(receivedAmount === amount) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(receivedAmount == amount) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 350000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 750000.msat, relayed) val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("B").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("B").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 350000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 750000.msat, outgoingSuccess) awaitCond(nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()) - assert(sent.length === 1, sent) - assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) + assert(sent.length == 1, sent) + assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) } test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") { @@ -550,45 +556,45 @@ class PaymentIntegrationSpec extends IntegrationSpec { val start = TimestampMilli.now() val (sender, eventListener) = (TestProbe(), TestProbe()) nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) - sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) - val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate - val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta))) + + sender.send(nodes("A").router, Router.GetRouterData) + val routingHints = List(sender.expectMsgType[Router.Data].privateChannels.head._2.toIncomingExtraHop.toList) val amount = 3000000000L.msat sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), Left("trampoline to non-trampoline is so #vintage"), extraHops = routingHints)) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) - assert(pr.paymentMetadata.nonEmpty) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) + assert(invoice.paymentMetadata.nonEmpty) - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((1500000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) sender.send(nodes("F").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.trampolineFees === 1000000.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.trampolineFees == 1500000.msat, paymentSent) - awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(receivedAmount === amount) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(receivedAmount == amount) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 1500000.msat, relayed) val outgoingSuccess = nodes("F").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("A").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("A").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 1500000.msat, outgoingSuccess) } test("send a trampoline payment B->D (temporary local failure at trampoline)") { @@ -596,26 +602,26 @@ class PaymentIntegrationSpec extends IntegrationSpec { // We put most of the capacity C <-> D on D's side. sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), Left("plz send everything"))) - val pr1 = sender.expectMsgType[PaymentRequest] - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams)) + val pr1 = sender.expectMsgType[Invoice] + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(sender.ref, 8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams)) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent](max = 30 seconds) // Now we try to send more than C's outgoing capacity to D. val amount = 2000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz Satoshi"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) @@ -625,18 +631,18 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz not Satoshi"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) - val payment = SendTrampolinePayment(amount, pr, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) @@ -691,8 +697,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { val channels1 = sender.expectMsgType[Relayer.OutgoingChannels] val channels2 = sender.expectMsgType[Relayer.OutgoingChannels] - logger.info(channels1.channels.map(_.toUsableBalance)) - logger.info(channels2.channels.map(_.toUsableBalance)) + logger.info(channels1.channels.map(_.toChannelBalance)) + logger.info(channels2.channels.map(_.toChannelBalance)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 2951239564..c85340b549 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair.integration import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived @@ -43,8 +44,8 @@ class PerformanceIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { val commonPerfTestConfig = ConfigFactory.parseMap(Map( - "eclair.max-funding-satoshis" -> 100_000_000, - "eclair.max-accepted-htlcs" -> Channel.MAX_ACCEPTED_HTLCS, + "eclair.channel.max-funding-satoshis" -> 100_000_000, + "eclair.channel.max-accepted-htlcs" -> Channel.MAX_ACCEPTED_HTLCS, "eclair.file-backup.enabled" -> false, ).asJava) @@ -83,9 +84,9 @@ class PerformanceIntegrationSpec extends IntegrationSpec { val amountMsat = 100_000.msat // first we retrieve a payment hash from B sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val pr = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(sender.ref, amountMsat, pr, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] sender.expectMsgType[PreimageReceived] val ps = sender.expectMsgType[PaymentSent] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala new file mode 100644 index 0000000000..2d13c8b4c5 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.integration + +import akka.testkit.TestProbe +import com.typesafe.config.ConfigFactory +import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq +import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCError} +import fr.acinq.eclair.{BitcoinDefaultWalletException, BitcoinWalletDisabledException, BitcoinWalletNotLoadedException, TestUtils} + +import scala.jdk.CollectionConverters._ + +/** + * Created by remyers on 16/03/2022. + */ + +class StartupIntegrationSpec extends IntegrationSpec { + + test("no bitcoind wallet configured and one wallet loaded") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + + test("no bitcoind wallet configured and two wallets loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + val thrown = intercept[BitcoinDefaultWalletException] { + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown == BitcoinDefaultWalletException(List(defaultWallet, ""))) + } + + test("explicit bitcoind wallet configured and two wallets loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + + test("explicit bitcoind wallet configured but not loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + val thrown = intercept[BitcoinWalletNotLoadedException] { + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "notloaded", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown == BitcoinWalletNotLoadedException("notloaded", List(defaultWallet, ""))) + } + + test("bitcoind started with wallets disabled") { + restartBitcoind(startupFlags = "-disablewallet", loadWallet = false) + val thrown = intercept[BitcoinWalletDisabledException] { + instantiateEclairNode("F", ConfigFactory.load().getConfig("eclair").withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown == BitcoinWalletDisabledException(e = JsonRPCError(Error(-32601, "Method not found")))) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala new file mode 100644 index 0000000000..c0cc5482f8 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -0,0 +1,59 @@ +package fr.acinq.eclair.integration.basic + +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.MilliSatoshiLong +import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import org.scalatest.TestData +import org.scalatest.concurrent.IntegrationPatience +import scodec.bits.HexStringSyntax + + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = ThreeNodesFixture + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) + ThreeNodesFixture(aliceParams, bobParams, carolParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("connect alice->bob and bob->carol, pay alice->carol") { f => + import f._ + connect(alice, bob) + connect(bob, carol) + + // we put watchers on auto pilot to confirm funding txs + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + + openChannel(alice, bob, 100_000 sat).channelId + openChannel(bob, carol, 100_000 sat).channelId + + // alice now knows about bob-carol + eventually { + val routerData = getRouterData(alice) + //prettyPrint(routerData, alice, bob, carol) + assert(routerData.channels.size == 2) // 2 channels + assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel + } + + sendSuccessfulPayment(alice, carol, 100_000 msat) + } + +} + + diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala new file mode 100644 index 0000000000..1e8a87c32f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -0,0 +1,95 @@ +package fr.acinq.eclair.integration.basic + +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL, RealScidStatus} +import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} +import org.scalatest.TestData +import org.scalatest.concurrent.IntegrationPatience +import scodec.bits.HexStringSyntax + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = TwoNodesFixture + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + TwoNodesFixture(aliceParams, bobParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("connect alice to bob") { f => + import f._ + connect(alice, bob) + } + + test("open a channel alice-bob") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + assert(getChannelState(alice, channelId) == NORMAL) + assert(getChannelState(bob, channelId) == NORMAL) + } + + test("open a channel alice-bob (autoconfirm)") { f => + import f._ + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + eventually { + assert(getChannelState(alice, channelId) == NORMAL) + assert(getChannelState(bob, channelId) == NORMAL) + } + } + + test("open a channel alice-bob and confirm deeply") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + confirmChannelDeep(alice, bob, channelId, BlockHeight(420_000), 21) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + } + + test("open a channel alice-bob and confirm deeply (autoconfirm)") { f => + import f._ + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + eventually { + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + } + } + + test("open a channel alice-bob and make a payment alice->bob") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + + eventually { + getRouterData(alice).privateChannels.size == 1 + } + + sendSuccessfulPayment(alice, bob, 10_000 msat) + } + +} + + diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala new file mode 100644 index 0000000000..3d304e4941 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala @@ -0,0 +1,97 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.ZeroConf +import fr.acinq.eclair.channel.ChannelTypes.AnchorOutputsZeroFeeHtlcTx +import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +/** + * Test the activation of zero-conf option, via features or channel type. + */ +class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = TwoNodesFixture + + val ZeroConfAlice = "zero_conf_alice" + val ZeroConfBob = "zero_conf_bob" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfAlice))(_ + (ZeroConf -> Optional)) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBob))(_ + (ZeroConf -> Optional)) + TwoNodesFixture(aliceParams, bobParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("open a channel alice-bob (zero-conf disabled on both sides)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + + assert(!getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(!getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + + test("open a channel alice-bob (zero-conf disabled on both sides, requested via channel type by alice)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + // bob rejects the channel + intercept[AssertionError] { + openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + } + } + + test("open a channel alice-bob (zero-conf enabled on bob, requested via channel type by alice)", Tag(ZeroConfBob)) { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + + assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + + test("open a channel alice-bob (zero-conf enabled on alice and bob, but not requested via channel type by alice)", Tag(ZeroConfAlice), Tag(ZeroConfBob)) { f => + import f._ + + assert(alice.nodeParams.features.activated.contains(ZeroConf)) + assert(bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false) + val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + + assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala new file mode 100644 index 0000000000..33bdf0d4c9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala @@ -0,0 +1,264 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens._ +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{ScidAlias, ZeroConf} +import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.RouteNotFound +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.wire.protocol.{FailureMessage, UnknownNextPeer, Update} +import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId} +import org.scalatest.OptionValues.convertOptionToValuable +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = ThreeNodesFixture + + val ZeroConfBobCarol = "zeroconf_bob_carol" + val ScidAliasBobCarol = "scid_alias_bob_carol" + val PublicBobCarol = "public_bob_carol" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + override def createFixture(testData: TestData): FixtureParam = { + // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. + val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Optional)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Optional)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Mandatory)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Mandatory)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + ThreeNodesFixture(aliceParams, bobParams, carolParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + private def createChannels(f: FixtureParam)(deepConfirm: Boolean): (ByteVector32, ByteVector32) = { + import f._ + + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + + connect(alice, bob) + connect(bob, carol) + + val channelId_ab = openChannel(alice, bob, 100_000 sat).channelId + val channelId_bc = openChannel(bob, carol, 100_000 sat).channelId + + (channelId_ab, channelId_bc) + } + + private def createBobToCarolTestHint(f: FixtureParam, useHint: Boolean, overrideHintScid_opt: Option[RealShortChannelId]): Seq[ExtraHop] = { + import f._ + if (useHint) { + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + // due to how node ids are built, bob < carol so carol is always the node 2 + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + // the hint is always using the alias + assert(carolHint.shortChannelId == bobAlias) + Seq(carolHint.modify(_.shortChannelId).setToIfDefined(overrideHintScid_opt)) + } else { + Seq.empty + } + } + + case object Ok + + private def sendPaymentAliceToCarol(f: FixtureParam, expected: Either[Either[Throwable, FailureMessage], Ok.type], useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): Unit = { + import f._ + val result = sendPayment(alice, carol, 100_000 msat, hints = Seq(createBobToCarolTestHint(f, useHint, overrideHintScid_opt))) match { + case Left(paymentFailed) => + Left(PaymentFailure.transformForUser(paymentFailed.failures).last match { + case LocalFailure(_, _, t) => Left(t) + case RemoteFailure(_, _, e) => Right(e.failureMessage) + case _: UnreadableRemoteFailure => fail("received unreadable remote failure") + }) + case Right(_) => Right(Ok) + } + + assert(result == expected) + } + + private def internalTest(f: FixtureParam, + deepConfirm: Boolean, + bcPublic: Boolean, + bcZeroConf: Boolean, + bcScidAlias: Boolean, + paymentWithoutHint: Either[Either[Throwable, FailureMessage], Ok.type], + paymentWithHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]], + paymentWithRealScidHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]]): Unit = { + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = deepConfirm) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf) == bcZeroConf) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias) == bcScidAlias) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFlags.announceChannel == bcPublic) + if (deepConfirm) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + } else if (bcZeroConf) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + } else { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + } + } + + eventually { + if (bcPublic && deepConfirm) { + // if channel bob-carol is public, we wait for alice to learn about it + val data = getRouterData(alice) + assert(data.channels.size == 2) + assert(data.channels.values.forall(pc => pc.update_1_opt.isDefined && pc.update_2_opt.isDefined)) + } + } + + eventually { + sendPaymentAliceToCarol(f, paymentWithoutHint) + } + + paymentWithHint_opt.foreach { paymentWithHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithHint, useHint = true) + } + } + + paymentWithRealScidHint_opt.foreach { paymentWithRealScidHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithRealScidHint, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + } + } + } + + test("a->b->c (b-c private)") { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = false, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) + paymentWithRealScidHint_opt = Some(Right(Ok)) // if alice uses the real scid instead of the bob-carol alias, it still works + ) + } + + test("a->b->c (b-c scid-alias private)", Tag(ScidAliasBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = true, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed private)", Tag(ZeroConfBobCarol)) { f => + internalTest(f, + deepConfirm = false, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed private)", Tag(ZeroConfBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + // TODO: we should be able to send payments with the real scid in the routing hint, but this currently doesn't work, + // because the ChannelRelayer relies on the the LocalChannelUpdate event to maintain its scid resolution map, and + // the channel doesn't emit a new one when a real scid is assigned, because we use the remote alias for the + // channel_update, not the real scid. So the channel_update remains the same. We used to have the ChannelRelayer + // also listen to ShortChannelIdAssigned event, but it's doesn't seem worth it here. + paymentWithRealScidHint_opt = None + ) + } + + test("a->b->c (b-c zero-conf scid-alias deeply confirmed private)", Tag(ZeroConfBobCarol), Tag(ScidAliasBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = true, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = false, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced yet + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWithoutHint = Right(Ok), + paymentWithHint_opt = None, // there is no routing hints for public channels + paymentWithRealScidHint_opt = None // there is no routing hints for public channels + ) + } + + test("temporary channel failures don't leak the real scid", Tag(ScidAliasBobCarol), Tag(ZeroConfBobCarol)) { f => + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = false) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + assert(getRouterData(bob).privateChannels.values.exists(_.nodeId2 == carol.nodeParams.nodeId)) + } + + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + assert(carolHint.shortChannelId == bobAlias) + + // We make sure Bob won't have enough liquidity to relay another payment. + sendSuccessfulPayment(bob, carol, 60_000_000 msat) + + // The channel update returned in failures doesn't leak the real scid. + val failure = sendFailingPayment(alice, carol, 50_000_000 msat, hints = Seq(Seq(carolHint))) + val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f } + assert(failureWithChannelUpdate.length == 1) + assert(failureWithChannelUpdate.head.update.shortChannelId == bobAlias) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala new file mode 100644 index 0000000000..5acfd6e32c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -0,0 +1,314 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps +import akka.actor.{ActorRef, ActorSystem} +import akka.testkit.{TestActor, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Transaction} +import fr.acinq.eclair.ShortChannelId.txIndex +import fr.acinq.eclair.blockchain.DummyOnChainWallet +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingConfirmedTriggered, WatchFundingDeeplyBuried, WatchFundingDeeplyBuriedTriggered} +import fr.acinq.eclair.channel.ChannelOpenResponse.ChannelOpened +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.io.PeerConnection.ConnectionResult +import fr.acinq.eclair.io.{Peer, PeerConnection, Switchboard} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} +import fr.acinq.eclair.payment.relay.{ChannelRelayer, Relayer} +import fr.acinq.eclair.payment.send.PaymentInitiator +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, PaymentSent} +import fr.acinq.eclair.router.Router +import fr.acinq.eclair.wire.protocol.IPAddress +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.{Assertions, EitherValues} + +import java.net.InetAddress +import java.util.UUID +import java.util.concurrent.atomic.AtomicLong +import scala.concurrent.duration.DurationInt +import scala.util.{Random, Try} + + +/** + * A minimal node setup, with real actors. + * + * Only the bitcoin watcher and wallet are mocked. + */ +case class MinimalNodeFixture private(nodeParams: NodeParams, + system: ActorSystem, + register: ActorRef, + router: ActorRef, + relayer: ActorRef, + switchboard: ActorRef, + paymentInitiator: ActorRef, + paymentHandler: ActorRef, + watcher: TestProbe, + wallet: DummyOnChainWallet) + +object MinimalNodeFixture extends Assertions with EitherValues { + + def nodeParamsFor(alias: String, seed: ByteVector32): NodeParams = { + NodeParams.makeNodeParams( + config = ConfigFactory.load().getConfig("eclair"), + instanceId = UUID.randomUUID(), + nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash), + channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash), + torAddress_opt = None, + database = TestDatabases.inMemoryDb(), + blockHeight = new AtomicLong(400_000), + feeEstimator = new TestFeeEstimator + ).modify(_.alias).setTo(alias) + .modify(_.chainHash).setTo(Block.RegtestGenesisBlock.hash) + .modify(_.routerConf.routerBroadcastInterval).setTo(1 second) + .modify(_.peerConnectionConf.maxRebroadcastDelay).setTo(1 second) + } + + def apply(nodeParams: NodeParams): MinimalNodeFixture = { + implicit val system: ActorSystem = ActorSystem(s"system-${nodeParams.alias}") + val readyListener = TestProbe("ready-listener") + system.eventStream.subscribe(readyListener.ref, classOf[SubscriptionsComplete]) + val bitcoinClient = new TestBitcoinCoreClient() + val wallet = new DummyOnChainWallet() + val watcher = TestProbe("watcher") + val watcherTyped = watcher.ref.toTyped[ZmqWatcher.Command] + val register = system.actorOf(Register.props(), "register") + val router = system.actorOf(Router.props(nodeParams, watcherTyped), "router") + val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register), "payment-handler") + val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler), "relayer") + val txPublisherFactory = Channel.SimpleTxPublisherFactory(nodeParams, watcherTyped, bitcoinClient) + val channelFactory = Peer.SimpleChannelFactory(nodeParams, watcherTyped, relayer, wallet, txPublisherFactory) + val peerFactory = Switchboard.SimplePeerFactory(nodeParams, wallet, channelFactory) + val switchboard = system.actorOf(Switchboard.props(nodeParams, peerFactory), "switchboard") + val paymentFactory = PaymentInitiator.SimplePaymentFactory(nodeParams, router, register) + val paymentInitiator = system.actorOf(PaymentInitiator.props(nodeParams, paymentFactory), "payment-initiator") + readyListener.expectMsgAllOf( + SubscriptionsComplete(classOf[Router]), + SubscriptionsComplete(classOf[Register]), + SubscriptionsComplete(classOf[Switchboard]), + SubscriptionsComplete(ChannelRelayer.getClass)) + MinimalNodeFixture( + nodeParams, + system, + register = register, + router = router, + relayer = relayer, + switchboard = switchboard, + paymentInitiator = paymentInitiator, + paymentHandler = paymentHandler, + watcher = watcher, + wallet = wallet + ) + } + + /** + * Connect node1 to node2, using a real [[PeerConnection]] and a fake transport layer. + * + * @param mutate12 a method to alter messages from node1 to node2 mid-flight for testing purposes + * @param mutate21 a method to alter messages from node2 to node1 mid-flight for testing purposes + */ + def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture, mutate12: Any => Any = identity, mutate21: Any => Any = identity)(implicit system: ActorSystem): ConnectionResult.Connected = { + val sender = TestProbe("sender") + + val connection1 = TestProbe("connection-1")(node1.system) + val transport1 = TestProbe("transport-1")(node1.system) + + val connection2 = TestProbe("connection-2")(node2.system) + val transport2 = TestProbe("transport-2")(node2.system) + + val peerConnection1 = node1.system.actorOf(PeerConnection.props(node1.nodeParams.keyPair, node1.nodeParams.peerConnectionConf, node1.switchboard, node1.router), s"peer-connection-${Random.nextLong()}") + val peerConnection2 = node2.system.actorOf(PeerConnection.props(node2.nodeParams.keyPair, node2.nodeParams.peerConnectionConf, node2.switchboard, node2.router), s"peer-connection-${Random.nextLong()}") + + transport1.setAutoPilot { (_: ActorRef, msg: Any) => + msg match { + case _: TransportHandler.Listener => TestActor.KeepRunning + case _: TransportHandler.ReadAck => TestActor.KeepRunning + case _ => + peerConnection2.tell(mutate12(msg), transport2.ref) + TestActor.KeepRunning + } + } + + transport2.setAutoPilot { (_: ActorRef, msg: Any) => + msg match { + case _: TransportHandler.Listener => TestActor.KeepRunning + case _: TransportHandler.ReadAck => TestActor.KeepRunning + case _ => + peerConnection1.tell(mutate21(msg), transport1.ref) + TestActor.KeepRunning + } + } + + val pendingAuth1 = PeerConnection.PendingAuth(connection1.ref, Some(node2.nodeParams.nodeId), IPAddress(InetAddress.getLoopbackAddress, 65432), origin_opt = Some(sender.ref), transport_opt = Some(transport1.ref), isPersistent = false) + peerConnection1 ! pendingAuth1 + + val pendingAuth2 = PeerConnection.PendingAuth(connection2.ref, None, IPAddress(InetAddress.getLoopbackAddress, 65432), origin_opt = None, transport_opt = Some(transport2.ref), isPersistent = false) + peerConnection2 ! pendingAuth2 + + peerConnection1 ! TransportHandler.HandshakeCompleted(node2.nodeParams.nodeId) + peerConnection2 ! TransportHandler.HandshakeCompleted(node1.nodeParams.nodeId) + + sender.expectMsgType[ConnectionResult.Connected] + } + + def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): ChannelOpened = { + val sender = TestProbe("sender") + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, channelType_opt, None, None, None)) + sender.expectMsgType[ChannelOpened] + } + + def fundingTx(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): Transaction = { + val fundingTxid = getChannelData(node, channelId).asInstanceOf[PersistentChannelData].commitments.commitInput.outPoint.txid + node.wallet.funded(fundingTxid) + } + + def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): Option[RealScidStatus.Temporary] = { + assert(getChannelState(node1, channelId) == WAIT_FOR_FUNDING_CONFIRMED) + val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + val fundingTx = data1Before.fundingTx.get + + val watch1 = node1.watcher.fishForMessage() { case w: WatchFundingConfirmed if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingConfirmed] + val watch2 = node2.watcher.fishForMessage() { case w: WatchFundingConfirmed if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingConfirmed] + + watch1.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) + watch2.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) + + eventually { + assert(getChannelState(node1, channelId) == NORMAL) + assert(getChannelState(node2, channelId) == NORMAL) + } + + val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + assert(realScid1 == realScid2) + Some(realScid1) + } + + def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): RealScidStatus.Final = { + assert(getChannelState(node1, channelId) == NORMAL) + val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val fundingTxid = data1Before.commitments.commitInput.outPoint.txid + val fundingTx = node1.wallet.funded(fundingTxid) + + val watch1 = node1.watcher.fishForMessage() { case w: WatchFundingDeeplyBuried if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingDeeplyBuried] + val watch2 = node2.watcher.fishForMessage() { case w: WatchFundingDeeplyBuried if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingDeeplyBuried] + + watch1.replyTo ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx) + watch2.replyTo ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx) + + waitReady(node1, channelId) + waitReady(node2, channelId) + + val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Final] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Final] + assert(realScid1 == realScid2) + realScid1 + } + + /** Utility method to make sure that the channel has processed all previous messages */ + def waitReady(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): Unit = { + getChannelState(node, channelId) + } + + def getChannelState(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelState = { + val sender = TestProbe("sender") + node.register ! Register.Forward(sender.ref.toTyped, channelId, CMD_GET_CHANNEL_STATE(sender.ref)) + sender.expectMsgType[RES_GET_CHANNEL_STATE].state + } + + def getChannelData(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelData = { + val sender = TestProbe("sender") + node.register ! Register.Forward(sender.ref.toTyped, channelId, CMD_GET_CHANNEL_DATA(sender.ref)) + sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data + } + + def getRouterData(node: MinimalNodeFixture)(implicit system: ActorSystem): Router.Data = { + val sender = TestProbe("sender") + sender.send(node.router, Router.GetRouterData) + sender.expectMsgType[Router.Data] + } + + /** + * Computes a deterministic [[RealShortChannelId]] based on a txid. We need this so that watchers can verify + * transactions in a independent and stateless fashion, since there is no actual blockchain in those tests. + */ + def deterministicShortId(txId: ByteVector32): RealShortChannelId = { + val blockHeight = txId.take(3).toInt(signed = false) + val txIndex = txId.takeRight(2).toInt(signed = false) + val outputIndex = 0 // funding txs created by the dummy wallet used in tests only have one output + RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) + } + + /** All known funding txs (we don't evaluate immediately because new ones could be created) */ + def knownFundingTxs(nodes: MinimalNodeFixture*): () => Iterable[Transaction] = () => nodes.map(_.wallet.funded.values).reduce(_ ++ _) + + /** + * An autopilot method for the watcher, that handled funding confirmation requests from the channel and channel + * validation requests from the router + */ + def watcherAutopilot(knownFundingTxs: () => Iterable[Transaction], deepConfirm: Boolean = true): TestActor.AutoPilot = (_, msg) => msg match { + case watch: ZmqWatcher.WatchFundingConfirmed => + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId) + .getOrElse(throw new RuntimeException(s"unknown fundingTxId=${watch.txId}, known=${knownFundingTxs().map(_.txid).mkString(",")}")) + watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) + TestActor.KeepRunning + case watch: ZmqWatcher.WatchFundingDeeplyBuried if deepConfirm => + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId).get + watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) + TestActor.KeepRunning + case vr: ZmqWatcher.ValidateRequest => + val res = Try { + val fundingTx = knownFundingTxs().find(tx => deterministicShortId(tx.txid) == vr.ann.shortChannelId) + .getOrElse(throw new RuntimeException(s"unknown realScid=${vr.ann.shortChannelId}, known=${knownFundingTxs().map(tx => deterministicShortId(tx.txid)).mkString(",")}")) + (fundingTx, ZmqWatcher.UtxoStatus.Unspent) + }.toEither + vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, res) + TestActor.KeepRunning + case _ => TestActor.KeepRunning + } + + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): Either[PaymentFailed, PaymentSent] = { + val sender = TestProbe("sender") + sender.send(node2.paymentHandler, MultiPartHandler.ReceivePayment(Some(amount), Left("test payment"))) + val invoice = sender.expectMsgType[Bolt11Invoice] + + val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = routeParams, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) + sender.expectMsgType[PaymentEvent] match { + case e: PaymentSent => Right(e) + case e: PaymentFailed => Left(e) + case e => fail(s"unexpected event $e") + } + } + + def sendSuccessfulPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { + sendPayment(node1, node2, amount, hints).value + } + + def sendFailingPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentFailed = { + sendPayment(node1, node2, amount, hints).left.value + } + + def prettyPrint(routerData: Router.Data, nodes: MinimalNodeFixture*): Unit = { + val nodeId2Alias = nodes.map(n => n.nodeParams.nodeId -> n.nodeParams.alias).toMap + .withDefault(nodeId => throw new RuntimeException(s"cannot resolve nodeId=$nodeId, make sure you have provided all node fixtures")) + routerData.channels.values.foreach { channel => + val name = Seq(channel.nodeId1, channel.nodeId2).map(nodeId2Alias).sorted.mkString("-") + val u1 = channel.update_1_opt.map(_ => s"${nodeId2Alias(channel.nodeId1)}=yes").getOrElse(s"${nodeId2Alias(channel.nodeId1)}=no") + val u2 = channel.update_2_opt.map(_ => s"${nodeId2Alias(channel.nodeId2)}=yes").getOrElse(s"${nodeId2Alias(channel.nodeId2)}=no") + println(s"$name : $u1 $u2") + } + } + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala new file mode 100644 index 0000000000..c2062d168f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala @@ -0,0 +1,31 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.ActorSystem +import akka.testkit.TestKit +import fr.acinq.eclair.NodeParams + + +case class ThreeNodesFixture(system: ActorSystem, + alice: MinimalNodeFixture, + bob: MinimalNodeFixture, + carol: MinimalNodeFixture) { + implicit val implicitSystem: ActorSystem = system + + def cleanup(): Unit = { + TestKit.shutdownActorSystem(alice.system) + TestKit.shutdownActorSystem(bob.system) + TestKit.shutdownActorSystem(carol.system) + TestKit.shutdownActorSystem(system) + } +} + +object ThreeNodesFixture { + def apply(aliceParams: NodeParams, bobParams: NodeParams, carolParams: NodeParams): ThreeNodesFixture = { + ThreeNodesFixture( + system = ActorSystem("system-test"), + alice = MinimalNodeFixture(aliceParams), + bob = MinimalNodeFixture(bobParams), + carol = MinimalNodeFixture(carolParams), + ) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala new file mode 100644 index 0000000000..4e9bc88287 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala @@ -0,0 +1,27 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.ActorSystem +import akka.testkit.TestKit +import fr.acinq.eclair.NodeParams + +case class TwoNodesFixture(system: ActorSystem, + alice: MinimalNodeFixture, + bob: MinimalNodeFixture) { + implicit val implicitSystem: ActorSystem = system + + def cleanup(): Unit = { + TestKit.shutdownActorSystem(alice.system) + TestKit.shutdownActorSystem(bob.system) + TestKit.shutdownActorSystem(system) + } +} + +object TwoNodesFixture { + def apply(aliceParams: NodeParams, bobParams: NodeParams): TwoNodesFixture = { + TwoNodesFixture( + system = ActorSystem("system-test"), + alice = MinimalNodeFixture(aliceParams), + bob = MinimalNodeFixture(bobParams) + ) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 4dedaa1129..fa6b642e34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -19,14 +19,15 @@ package fr.acinq.eclair.interop.rustytests import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.wire.protocol.Init import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestFeeEstimator, TestKitBaseClass, TestUtils} @@ -74,7 +75,7 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -106,15 +107,15 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu TestKit.shutdownActorSystem(system) } - test("01-offer1") { f => assert(f.ref === f.res) } - test("02-offer2") { f => assert(f.ref === f.res) } - test("03-fulfill1") { f => assert(f.ref === f.res) } - // test("04-two-commits-onedir") { f => assert(f.ref === f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes - // test("05-two-commits-in-flight") { f => assert(f.ref === f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) - test("10-offers-crossover") { f => assert(f.ref === f.res) } - test("11-commits-crossover") { f => assert(f.ref === f.res) } - /*test("13-fee") { f => assert(f.ref === f.res)} - test("14-fee-twice") { f => assert(f.ref === f.res)} - test("15-fee-twice-back-to-back") { f => assert(f.ref === f.res)}*/ + test("01-offer1") { f => assert(f.ref == f.res) } + test("02-offer2") { f => assert(f.ref == f.res) } + test("03-fulfill1") { f => assert(f.ref == f.res) } + // test("04-two-commits-onedir") { f => assert(f.ref == f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes + // test("05-two-commits-in-flight") { f => assert(f.ref == f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) + test("10-offers-crossover") { f => assert(f.ref == f.res) } + test("11-commits-crossover") { f => assert(f.ref == f.res) } + /*test("13-fee") { f => assert(f.ref == f.res)} + test("14-fee-twice") { f => assert(f.ref == f.res)} + test("15-fee-twice-back-to-back") { f => assert(f.ref == f.res)}*/ } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 6056097a9f..4fba26eb41 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -21,7 +21,7 @@ import java.util.UUID import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, TestConstants, TestUtils} @@ -81,7 +81,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging fout.newLine() exec(rest, a, b) case dump(x) :: rest => - resolve(x) ! CMD_GETSTATEDATA(ActorRef.noSender) + resolve(x) ! CMD_GET_CHANNEL_DATA(ActorRef.noSender) context.become(wait(a, b, script)) case "" :: rest => exec(rest, a, b) @@ -126,7 +126,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging a forward msg unstashAll() exec(script.drop(1), a, b) - case RES_GETSTATEDATA(d: DATA_NORMAL) if script.head.endsWith(":dump") => + case RES_GET_CHANNEL_DATA(d: DATA_NORMAL) if script.head.endsWith(":dump") => def rtrim(s: String) = s.replaceAll("\\s+$", "") import d.commitments._ val l = List( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala index 58682d6074..202a4fe44f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.io.MessageRelay._ import fr.acinq.eclair.io.Peer.{PeerInfo, PeerNotFound} @@ -56,38 +56,38 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("relay with new connection") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.Connected(peerConnection.ref.toClassic, peer.ref.toClassic) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("relay with existing peer") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.AlreadyConnected(peerConnection.ref.toClassic, peer.ref.toClassic) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("can't open new connection") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, Some(probe.ref)) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.NoAddressFound probe.expectMessage(ConnectionFailure(messageId, PeerConnection.ConnectionResult.NoAddressFound)) } @@ -95,13 +95,13 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("no channel with previous node") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) val getPeerInfo = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo.remoteNodeId === previousNodeId) + assert(getPeerInfo.remoteNodeId == previousNodeId) getPeerInfo.replyTo ! PeerInfo(peer.ref.toClassic, previousNodeId, Peer.CONNECTED, None, 0) probe.expectMessage(AgainstPolicy(messageId, RelayChannelsOnly)) @@ -111,17 +111,17 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("no channel with next node") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) val getPeerInfo1 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo1.remoteNodeId === previousNodeId) + assert(getPeerInfo1.remoteNodeId == previousNodeId) getPeerInfo1.replyTo ! PeerInfo(peer.ref.toClassic, previousNodeId, Peer.CONNECTED, None, 1) val getPeerInfo2 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo2.remoteNodeId === bobId) + assert(getPeerInfo2.remoteNodeId == bobId) getPeerInfo2.replyTo ! PeerNotFound(bobId) probe.expectMessage(AgainstPolicy(messageId, RelayChannelsOnly)) @@ -131,26 +131,26 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("channels on both ends") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, None) val getPeerInfo1 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo1.remoteNodeId === previousNodeId) + assert(getPeerInfo1.remoteNodeId == previousNodeId) getPeerInfo1.replyTo ! PeerInfo(TestProbe()(system.classicSystem).ref, previousNodeId, Peer.CONNECTED, None, 1) val getPeerInfo2 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo2.remoteNodeId === bobId) + assert(getPeerInfo2.remoteNodeId == bobId) getPeerInfo2.replyTo ! PeerInfo(peer.ref.toClassic, bobId, Peer.CONNECTED, None, 2) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("no relay") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, NoRelay, Some(probe.ref)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index d9ac3df090..949e24bdad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -22,125 +22,56 @@ import org.scalatest.funsuite.AnyFunSuite class NodeURISpec extends AnyFunSuite { val PUBKEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134" - val SHORT_PUB_KEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca" - val NOT_HEXA_PUB_KEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fcghijklmn" val IPV4_ENDURANCE = "34.250.234.192" val NAME_ENDURANCE = "endurance.acinq.co" val IPV6 = "[2405:204:66a9:536c:873f:dc4a:f055:a298]" - val IPV6_NO_BRACKETS = "2001:db8:a0b:12f0::1" - val IPV6_PREFIX = "[2001:db8:a0b:12f0::1/64]" - val IPV6_ZONE_IDENTIFIER = "[2001:db8:a0b:12f0::1%eth0]" test("default port") { assert(NodeURI.DEFAULT_PORT == 9735) } - // ---------- IPV4 - - test("parse NodeURI with IPV4 and port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.nodeId.toString() == PUBKEY) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV4 and NO port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - test("parse NodeURI with named host and port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.nodeId.toString() == PUBKEY) - assert(nodeUri.address.getPort == 9737) - } - - // ---------- IPV6 / regular with brackets - - test("parse NodeURI with IPV6 with brackets and port") { - val uri = s"$PUBKEY@$IPV6:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with brackets and NO port") { - val uri = s"$PUBKEY@$IPV6" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / regular without brackets - - test("fail to parse NodeURI with IPV6 without brackets and port, and use default port") { - // this can not be parsed because we can not tell what the port is (brackets are required) and the port is the default - val uri = s"$PUBKEY@$IPV6_NO_BRACKETS:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - test("parse NodeURI with IPV6 without brackets and NO port") { - val uri = s"$PUBKEY@$IPV6_NO_BRACKETS" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / prefix - - test("parse NodeURI with IPV6 with prefix and port") { - val uri = s"$PUBKEY@$IPV6_PREFIX:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with prefix and NO port") { - val uri = s"$PUBKEY@$IPV6_PREFIX" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / zone identifier - - test("parse NodeURI with IPV6 with a zone identifier and port") { - val uri = s"$PUBKEY@$IPV6_ZONE_IDENTIFIER:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with a zone identifier and NO port") { - val uri = s"$PUBKEY@$IPV6_ZONE_IDENTIFIER" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- fail if public key is not valid - - test("parsing should fail if the public key is not correct") { - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@$IPV4_ENDURANCE")) - intercept[IllegalArgumentException](NodeURI.parse(s"$NOT_HEXA_PUB_KEY@$IPV4_ENDURANCE")) + test("NodeURI parsing success") { + case class TestCase(uri: String, formattedAddr: String, port: Int) + + val testCases = List( + TestCase(s"$PUBKEY@$IPV4_ENDURANCE:9737", IPV4_ENDURANCE, 9737), + TestCase(s"$PUBKEY@$IPV4_ENDURANCE", IPV4_ENDURANCE, 9735), + TestCase(s"$PUBKEY@$NAME_ENDURANCE:9737", "13.248.222.197", 9737), + TestCase(s"$PUBKEY@$NAME_ENDURANCE", "13.248.222.197", 9735), + TestCase(s"$PUBKEY@$IPV6:9737", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9737), + TestCase(s"$PUBKEY@$IPV6", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9735), + ) + + for (testCase <- testCases) { + val nodeUri = NodeURI.parse(testCase.uri) + assert(nodeUri.nodeId.toString() == PUBKEY) + assert(nodeUri.address.host == testCase.formattedAddr) + assert(nodeUri.address.port == testCase.port) + assert(nodeUri.toString == s"$PUBKEY@${testCase.formattedAddr}:${testCase.port}") + } } - // ---------- fail if host:port is not valid - - test("parsing should fail if host:port is not valid") { - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@1.2.3.4:abcd")) - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@1.2.3.4:999999999999999999999")) - } - - test("parsing should fail if the uri is malformed") { - intercept[IllegalArgumentException](NodeURI.parse("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@")) - intercept[IllegalArgumentException](NodeURI.parse("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@123.45@654321")) - intercept[IllegalArgumentException](NodeURI.parse("loremipsum")) - intercept[IllegalArgumentException](NodeURI.parse(IPV6)) - intercept[IllegalArgumentException](NodeURI.parse(IPV4_ENDURANCE)) - intercept[IllegalArgumentException](NodeURI.parse(PUBKEY)) - intercept[IllegalArgumentException](NodeURI.parse("")) - intercept[IllegalArgumentException](NodeURI.parse("@")) - intercept[IllegalArgumentException](NodeURI.parse(":")) + test("NodeURI parsing failure") { + val testCases = List( + s"03933884aaf1d6b108397e5efe5c86bcf2d8ca@$IPV4_ENDURANCE", + s"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fcghijklmn@$IPV4_ENDURANCE", + s"$PUBKEY@1.2.3.4:abcd", + s"$PUBKEY@1.2.3.4:999999999999999999999", + "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@", + "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@123.45@654321", + "loremipsum", + IPV6, + IPV4_ENDURANCE, + PUBKEY, + "", + "@", + ":", + ) + for (testCase <- testCases) { + intercept[IllegalArgumentException](NodeURI.parse(testCase)) + } } - } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index d2883aa285..5985109de1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -18,11 +18,12 @@ package fr.acinq.eclair.io import akka.actor.PoisonPill import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -42,7 +43,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi def ipv4FromInet4(address: InetSocketAddress): IPv4 = IPv4.apply(address.getAddress.asInstanceOf[Inet4Address], address.getPort) - val address = new InetSocketAddress("localhost", 42000) + val address = NodeAddress.fromParts("localhost", 42000).get val fakeIPAddress = NodeAddress.fromParts("1.2.3.4", 42000).get // this map will store private keys so that we can sign new announcements at will val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty @@ -67,16 +68,16 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer))) } - def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features), doSync: Boolean = false, isPersistent: Boolean = true): Unit = { + def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures()), doSync: Boolean = false, isPersistent: Boolean = true): Unit = { // let's simulate a connection val probe = TestProbe() probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = isPersistent)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features, doSync)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] - assert(localInit.networks === List(Block.RegtestGenesisBlock.hash)) + assert(localInit.networks == List(Block.RegtestGenesisBlock.hash)) transport.send(peerConnection, remoteInit) transport.expectMsgType[TransportHandler.ReadAck] if (doSync) { @@ -85,7 +86,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi router.expectNoMessage(1 second) } peer.expectMsg(PeerConnection.ConnectionReady(peerConnection, remoteNodeId, address, outgoing = true, localInit, remoteInit)) - assert(peerConnection.stateName === PeerConnection.CONNECTED) + assert(peerConnection.stateName == PeerConnection.CONNECTED) } test("establish connection") { f => @@ -96,15 +97,15 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("send incoming connection's remote address in init") { f => import f._ val probe = TestProbe() - val incomingConnection = PeerConnection.PendingAuth(connection.ref, None, fakeIPAddress.socketAddress, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = true) + val incomingConnection = PeerConnection.PendingAuth(connection.ref, None, fakeIPAddress, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = true) assert(!incomingConnection.outgoing) probe.send(peerConnection, incomingConnection) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = false)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] - assert(localInit.remoteAddress_opt === Some(fakeIPAddress)) + assert(localInit.remoteAddress_opt == Some(fakeIPAddress)) } test("handle connection closed during authentication") { f => @@ -133,7 +134,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(peerConnection) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) probe.expectTerminated(peerConnection, nodeParams.peerConnectionConf.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out")) } @@ -145,7 +146,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value) @@ -161,7 +162,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value) @@ -177,7 +178,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] // remote activated MPP but forgot payment secret @@ -194,10 +195,10 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] - transport.send(peerConnection, protocol.Init(Bob.nodeParams.features, TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil)))) + transport.send(peerConnection, protocol.Init(Bob.nodeParams.features.initFeatures(), TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil)))) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible networks")) @@ -215,7 +216,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val ping = Ping(42, randomBytes(127)) transport.send(peerConnection, ping) transport.expectMsg(TransportHandler.ReadAck(ping)) - assert(transport.expectMsgType[Pong].data.size === ping.pongLength) + assert(transport.expectMsgType[Pong].data.size == ping.pongLength) } test("send a ping if no message after init") { f => @@ -246,7 +247,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val ping = Ping(Int.MaxValue, randomBytes(127)) transport.send(peerConnection, ping) transport.expectMsg(TransportHandler.ReadAck(ping)) - assert(transport.expectMsgType[Warning].channelId === Peer.CHANNELID_ZERO) + assert(transport.expectMsgType[Warning].channelId == Peer.CHANNELID_ZERO) transport.expectNoMessage() } @@ -333,7 +334,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val query = QueryShortChannelIds( Alice.nodeParams.chainHash, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42000))), TlvStream.empty) // make sure that routing messages go through @@ -376,14 +377,14 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi router.send(peerConnection, GossipDecision.InvalidAnnouncement(channels(0))) // peer will return a connection-wide error, including the hex-encoded representation of the bad message val warn1 = transport.expectMsgType[Warning] - assert(warn1.channelId === Peer.CHANNELID_ZERO) + assert(warn1.channelId == Peer.CHANNELID_ZERO) assert(new String(warn1.data.toArray).startsWith("invalid announcement, couldn't verify channel")) // let's assume that one of the sigs were invalid router.send(peerConnection, GossipDecision.InvalidSignature(channels(0))) // peer will return a connection-wide error, including the hex-encoded representation of the bad message val warn2 = transport.expectMsgType[Warning] - assert(warn2.channelId === Peer.CHANNELID_ZERO) + assert(warn2.channelId == Peer.CHANNELID_ZERO) assert(new String(warn2.data.toArray).startsWith("invalid announcement sig")) } @@ -391,7 +392,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) probe watch peerConnection probe.expectTerminated(peerConnection, max = 1500 millis) @@ -403,22 +404,16 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.expectMsg(()) } - test("keep using transient connection") { f => + test("keep transient connection open for a short while") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) - probe.send(peerConnection, message) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe watch peerConnection - sleep(900 millis) - assert(peerConnection.stateName === PeerConnection.CONNECTED) - probe.send(peerConnection, message) - sleep(900 millis) - assert(peerConnection.stateName === PeerConnection.CONNECTED) probe.send(peerConnection, message) - sleep(900 millis) - assert(peerConnection.stateName === PeerConnection.CONNECTED) - sleep(200 millis) + // The connection is still open for a short while. + assert(peerConnection.stateName == PeerConnection.CONNECTED) + sleep(1 second) probe.expectTerminated(peerConnection, max = Duration.Zero) } @@ -426,10 +421,10 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) - assert(peerConnection.stateName === PeerConnection.CONNECTED) - probe.send(peerConnection, FundingLocked(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) + assert(peerConnection.stateName == PeerConnection.CONNECTED) + probe.send(peerConnection, ChannelReady(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) peerConnection.stateData match { case d: PeerConnection.ConnectedData => assert(d.isPersistent) case _ => fail() @@ -439,7 +434,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("incoming rate limiting") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = true) - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(nodeParams.nodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(nodeParams.nodeId, None), Nil) for (_ <- 1 to 30) { transport.send(peerConnection, message) } @@ -458,7 +453,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("outgoing rate limiting") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = true) - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) for (_ <- 1 to 30) { peer.send(peerConnection, message) } @@ -474,7 +469,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.expectMsg(message) } - test("filter private IP addresses") { _ => + test("filter private IP addresses") { () => val testCases = Seq( NodeAddress.fromParts("127.0.0.1", 9735).get -> false, NodeAddress.fromParts("0.0.0.0", 9735).get -> false, @@ -487,7 +482,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi ) for ((address, expected) <- testCases) { val isPublicIP = NodeAddress.isPublicIPAddress(address) - assert(isPublicIP === expected) + assert(isPublicIP == expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index d30af3c8a2..de1b274936 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -20,9 +20,8 @@ import akka.actor.Status.Failure import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{ActorContext, ActorRef, FSM, PoisonPill, Status} import akka.testkit.{TestFSMRef, TestProbe} -import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ @@ -31,6 +30,7 @@ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsTags import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -55,7 +55,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle case class FakeChannelFactory(channel: TestProbe) extends ChannelFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): ActorRef = { - assert(remoteNodeId === Bob.nodeParams.nodeId) + assert(remoteNodeId == Bob.nodeParams.nodeId) channel.ref } } @@ -74,7 +74,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcement")) { @@ -86,17 +86,17 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, peer, peerConnection, channel, switchboard))) } - def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features)): Unit = { + def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[PersistentChannelData] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures())): Unit = { // let's simulate a connection switchboard.send(peer, Peer.Init(channels)) - val localInit = protocol.Init(peer.underlyingActor.nodeParams.features) - switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, localInit, remoteInit)) + val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) + switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, localInit, remoteInit)) val probe = TestProbe() probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo.peer === peer) - assert(peerInfo.nodeId === remoteNodeId) - assert(peerInfo.state === Peer.CONNECTED) + assert(peerInfo.peer == peer) + assert(peerInfo.nodeId == remoteNodeId) + assert(peerInfo.state == Peer.CONNECTED) } test("restore existing channels") { f => @@ -104,7 +104,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) probe.send(peer, Peer.GetPeerInfo(None)) - probe.expectMsg(PeerInfo(peer, remoteNodeId, Peer.CONNECTED, Some(fakeIPAddress.socketAddress), 1)) + probe.expectMsg(PeerInfo(peer, remoteNodeId, Peer.CONNECTED, Some(fakeIPAddress), 1)) } test("fail to connect if no address provided or found") { f => @@ -124,12 +124,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // we create a dummy tcp server and update bob's announcement to point to it val (mockServer, serverAddress) = createMockServer() - val mockAddress = HostAndPort.fromParts(serverAddress.getHostName, serverAddress.getPort) + val mockAddress_opt = NodeAddress.fromParts(serverAddress.getHostName, serverAddress.getPort).toOption val probe = TestProbe() probe.send(peer, Peer.Init(Set.empty)) // we have auto-reconnect=false so we need to manually tell the peer to reconnect - probe.send(peer, Peer.Connect(remoteNodeId, Some(mockAddress), probe.ref, isPersistent = true)) + probe.send(peer, Peer.Connect(remoteNodeId, mockAddress_opt, probe.ref, isPersistent = true)) // assert our mock server got an incoming connection (the client was spawned with the address from node_announcement) awaitCond(mockServer.accept() != null, max = 30 seconds, interval = 1 second) @@ -188,7 +188,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) - assert(probe.expectMsgType[Peer.PeerInfo].state === Peer.CONNECTED) + assert(probe.expectMsgType[Peer.PeerInfo].state == Peer.CONNECTED) probe.send(peer, Peer.Disconnect(f.remoteNodeId)) probe.expectMsg("disconnecting") @@ -202,11 +202,11 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle awaitCond { probe.send(peer, Peer.GetPeerInfo(None)) - probe.expectMsgType[Peer.PeerInfo].state === Peer.DISCONNECTED + probe.expectMsgType[Peer.PeerInfo].state == Peer.DISCONNECTED } probe.send(peer, Peer.Disconnect(f.remoteNodeId)) - assert(probe.expectMsgType[Status.Failure].cause.getMessage === "not connected") + assert(probe.expectMsgType[Status.Failure].cause.getMessage == "not connected") } test("handle new connection in state CONNECTED") { f => @@ -220,23 +220,23 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle channel.expectMsg(INPUT_RESTORED(ChannelCodecsSpec.normal)) val (localInit, remoteInit) = { val inputReconnected = channel.expectMsgType[INPUT_RECONNECTED] - assert(inputReconnected.remote === peerConnection1.ref) + assert(inputReconnected.remote == peerConnection1.ref) (inputReconnected.localInit, inputReconnected.remoteInit) } - peerConnection2.send(peer, PeerConnection.ConnectionReady(peerConnection2.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = false, localInit, remoteInit)) + peerConnection2.send(peer, PeerConnection.ConnectionReady(peerConnection2.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit)) // peer should kill previous connection peerConnection1.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) channel.expectMsg(INPUT_RECONNECTED(peerConnection2.ref, localInit, remoteInit)) - awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection === peerConnection2.ref) + awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection == peerConnection2.ref) - peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = false, localInit, remoteInit)) + peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit)) // peer should kill previous connection peerConnection2.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) channel.expectMsg(INPUT_RECONNECTED(peerConnection3.ref, localInit, remoteInit)) - awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection === peerConnection3.ref) + awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection == peerConnection3.ref) } test("send state transitions to child reconnection actor", Tag("auto_reconnect"), Tag("with_node_announcement")) { f => @@ -257,8 +257,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.WAITING, ReconnectionTask.CONNECTING)) // we simulate a success - val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features) - probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, dummyInit, dummyInit)) + val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) + probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, dummyInit, dummyInit)) // we make sure that the reconnection task has done a full circle monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.CONNECTING, ReconnectionTask.IDLE)) @@ -275,14 +275,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage() peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId === open.temporaryChannelId) + assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId == open.temporaryChannelId) channel.expectMsg(open) // open_channel messages with the same temporary channel id should simply be ignored peerConnection.send(peer, open.copy(fundingSatoshis = 100000 sat, fundingPubkey = randomKey().publicKey)) channel.expectNoMessage(100 millis) peerConnection.expectNoMessage(100 millis) - assert(peer.stateData.channels.size === 1) + assert(peer.stateData.channels.size == 1) } test("don't spawn a wumbo channel if wumbo feature isn't enabled") { f => @@ -341,7 +341,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They only support anchor outputs with zero fee htlc txs and we don't. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) } @@ -353,7 +353,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They want to use a channel type we don't support yet. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map(StaticRemoteKey -> Mandatory), unknown = Set(UnknownFeature(22))))))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard")) } @@ -379,7 +379,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))) peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType === ChannelTypes.Standard) + assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType == ChannelTypes.Standard) channel.expectMsg(open) } @@ -391,15 +391,15 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle assert(peer.stateData.channels.isEmpty) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.StaticRemoteKey) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.StaticRemoteKey) // We can create channels that don't use the features we have enabled. probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.Standard), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.Standard) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.Standard) // We can create channels that use features that we haven't enabled. probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.AnchorOutputs), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.AnchorOutputs) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.AnchorOutputs) } test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f => @@ -414,10 +414,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.AnchorOutputs) - assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.channelType == ChannelTypes.AnchorOutputs) + assert(init.fundingAmount == 15000.sat) + assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => @@ -432,10 +432,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) - assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + assert(init.fundingAmount == 15000.sat) + assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => @@ -445,9 +445,17 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.StaticRemoteKey) + assert(init.channelType == ChannelTypes.StaticRemoteKey) assert(init.localParams.walletStaticPaymentBasepoint.isDefined) - assert(init.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) + assert(init.localParams.defaultFinalScriptPubKey == Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) + } + + test("do not allow option_scid_alias with public channel") { f => + import f._ + + intercept[IllegalArgumentException] { + Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, Some(ChannelFlags(announceChannel = true)), None) + } } test("set origin_opt when spawning a channel") { f => @@ -456,7 +464,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() val channelFactory = new ChannelFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): ActorRef = { - assert(origin_opt === Some(probe.ref)) + assert(origin_opt.contains(probe.ref)) channel.ref } } @@ -464,8 +472,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.fundingAmount === 15000.sat) - assert(init.pushAmount === 100.msat) + assert(init.fundingAmount == 15000.sat) + assert(init.pushAmount == 100.msat) } test("handle final channelId assigned in state DISCONNECTED") { f => @@ -475,13 +483,13 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peer ! ConnectionDown(peerConnection.ref) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo1 = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo1.state === Peer.DISCONNECTED) - assert(peerInfo1.channels === 1) + assert(peerInfo1.state == Peer.DISCONNECTED) + assert(peerInfo1.channels == 1) peer ! ChannelIdAssigned(probe.ref, remoteNodeId, randomBytes32(), randomBytes32()) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo2 = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo2.state === Peer.DISCONNECTED) - assert(peerInfo2.channels === 2) + assert(peerInfo2.state == Peer.DISCONNECTED) + assert(peerInfo2.channels == 2) } test("notify when last channel is closed") { f => @@ -506,7 +514,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle test("reply to relay request") { f => import f._ connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) - val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) val messageId = randomBytes32() val probe = TestProbe() peer ! RelayOnionMessage(messageId, msg, Some(probe.ref.toTyped)) @@ -515,7 +523,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle test("reply to relay request disconnected") { f => import f._ - val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) val messageId = randomBytes32() val probe = TestProbe() peer ! RelayOnionMessage(messageId, msg, Some(probe.ref.toTyped)) @@ -534,7 +542,7 @@ object PeerSpec { } def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = { - protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private, openTlv) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index d00562c2b7..1e4721819b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.io import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair._ import fr.acinq.eclair.io.Peer.ChannelId import fr.acinq.eclair.io.ReconnectionTask.WaitingData @@ -38,7 +38,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike private val PeerNothingData = Peer.Nothing private val PeerDisconnectedData = Peer.DisconnectedData(channels) - private val PeerConnectedData = Peer.ConnectedData(fakeIPAddress.socketAddress, system.deadLetters, null, null, channels.map { case (k: ChannelId, v) => (k, v) }) + private val PeerConnectedData = Peer.ConnectedData(fakeIPAddress, system.deadLetters, null, null, channels.map { case (k: ChannelId, v) => (k, v) }) case class FixtureParam(nodeParams: NodeParams, remoteNodeId: PublicKey, reconnectionTask: TestFSMRef[ReconnectionTask.State, ReconnectionTask.Data, ReconnectionTask], monitor: TestProbe) @@ -101,7 +101,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike peer.send(reconnectionTask, Peer.Transition(PeerNothingData, PeerDisconnectedData)) val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.WAITING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, connectingData: ReconnectionTask.ConnectingData) = monitor.expectMsgType[TransitionWithData] - assert(connectingData.to === fakeIPAddress.socketAddress) + assert(connectingData.to == fakeIPAddress) val expectedNextReconnectionDelayInterval = (nodeParams.maxReconnectInterval.toSeconds / 2) to nodeParams.maxReconnectInterval.toSeconds assert(expectedNextReconnectionDelayInterval contains connectingData.nextReconnectionDelay.toSeconds) // we only reconnect once } @@ -134,13 +134,13 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.CONNECTING, ReconnectionTask.WAITING, _, waitingData1: WaitingData) = monitor.expectMsgType[TransitionWithData](60 seconds) - assert(waitingData1.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 2)) + assert(waitingData1.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 2)) probe.send(reconnectionTask, ReconnectionTask.TickReconnect) val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.CONNECTING, ReconnectionTask.WAITING, _, waitingData2: WaitingData) = monitor.expectMsgType[TransitionWithData](60 seconds) - assert(waitingData2.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 4)) + assert(waitingData2.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 4)) probe.send(reconnectionTask, ReconnectionTask.TickReconnect) val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] @@ -153,7 +153,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // the auto reconnect kicks off again, but this time we pick up the reconnect delay where we left it val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.WAITING, _, waitingData3: WaitingData) = monitor.expectMsgType[TransitionWithData] - assert(waitingData3.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 8)) + assert(waitingData3.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 8)) } test("all kind of connection failures should be caught by the reconnection task", Tag("auto_reconnect")) { f => @@ -225,7 +225,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike mockServer.close() } - test("select peer address for reconnection") { _ => + test("select peer address for reconnection") { () => val nodeParams = mock[NodeParams] val clearnet = NodeAddress.fromParts("1.2.3.4", 9735).get val tor = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get @@ -235,9 +235,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // tor not supported: always return clearnet addresses nodeParams.socksProxy_opt returns None - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === None) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == None) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(clearnet)) } { @@ -247,9 +247,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike socksParams.useForIPv4 returns false socksParams.useForIPv6 returns false nodeParams.socksProxy_opt returns Some(socksParams) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === Some(tor)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(clearnet)) } { @@ -259,9 +259,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike socksParams.useForIPv4 returns true socksParams.useForIPv6 returns true nodeParams.socksProxy_opt returns Some(socksParams) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === Some(tor)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(tor)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index 3ec6f4a285..c20f33814b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -3,14 +3,14 @@ package fr.acinq.eclair.io import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{Actor, ActorContext, ActorRef, Props, Status} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector64 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.channel.ChannelIdAssigned import fr.acinq.eclair.io.Switchboard._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} +import fr.acinq.eclair.{Features, InitFeature, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -42,8 +42,8 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { probe.expectMsg(remoteNodeId) peer.expectMsg(Peer.Init(Set.empty)) val connect = peer.expectMsgType[Peer.Connect] - assert(connect.nodeId === remoteNodeId) - assert(connect.address_opt === None) + assert(connect.nodeId == remoteNodeId) + assert(connect.address_opt == None) } test("disconnect from peers") { @@ -58,19 +58,19 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { val unknownNodeId = randomKey().publicKey probe.send(switchboard, Peer.Disconnect(unknownNodeId)) - assert(probe.expectMsgType[Status.Failure].cause.getMessage === s"peer $unknownNodeId not found") + assert(probe.expectMsgType[Status.Failure].cause.getMessage == s"peer $unknownNodeId not found") probe.send(switchboard, Peer.Disconnect(remoteNodeId)) peer.expectMsg(Peer.Disconnect(remoteNodeId)) } - def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features, expectedSync: Boolean): Unit = { + def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features[InitFeature], expectedSync: Boolean): Unit = { val (probe, peer, peerConnection) = (TestProbe(), TestProbe(), TestProbe()) val switchboard = TestActorRef(new Switchboard(nodeParams, FakePeerFactory(probe, peer))) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection.chainHash === nodeParams.chainHash) - assert(initConnection.features === expectedFeatures) - assert(initConnection.doSync === expectedSync) + assert(initConnection.chainHash == nodeParams.chainHash) + assert(initConnection.features == expectedFeatures) + assert(initConnection.doSync == expectedSync) } test("sync if no whitelist is defined and peer has channels") { @@ -90,16 +90,16 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { switchboard ! ChannelIdAssigned(TestProbe().ref, remoteNodeId, randomBytes32(), randomBytes32()) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection1.chainHash === nodeParams.chainHash) - assert(initConnection1.features === nodeParams.features.initFeatures()) + assert(initConnection1.chainHash == nodeParams.chainHash) + assert(initConnection1.features == nodeParams.features.initFeatures()) assert(initConnection1.doSync) // We don't have channels with our peer, so we won't trigger a sync when connecting. switchboard ! LastChannelClosed(peer.ref, remoteNodeId) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection2.chainHash === nodeParams.chainHash) - assert(initConnection2.features === nodeParams.features.initFeatures()) + assert(initConnection2.chainHash == nodeParams.chainHash) + assert(initConnection2.features == nodeParams.features.initFeatures()) assert(!initConnection2.doSync) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index e9962f5685..59a05f2d0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -17,15 +17,15 @@ package fr.acinq.eclair.json import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Btc, ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair._ import fr.acinq.eclair.balance.CheckBalance import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, GlobalBalance, MainAndHtlcBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.PeerInfo -import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} +import fr.acinq.eclair.payment.{Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs @@ -51,7 +51,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val error = intercept[org.json4s.MappingException] { JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats) } - assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) + assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.scalacompat.OutPoint. Consider implementing a CustomKeySerializer.")) // but it works with our custom key serializer val json = JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats + ByteVectorSerializer + OutPointKeySerializer) @@ -70,7 +70,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val error = intercept[org.json4s.MappingException] { JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats) } - assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) + assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.scalacompat.OutPoint. Consider implementing a CustomKeySerializer.")) // but it works with our custom key serializer val json = JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats + TransactionSerializer + OutPointKeySerializer) @@ -90,13 +90,15 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { } test("NodeAddress serialization") { - val ipv4 = NodeAddress.fromParts("10.0.0.1", 8888).get - val ipv6LocalHost = NodeAddress.fromParts(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)).getHostAddress, 9735).get + val ipv4 = IPAddress(InetAddress.getByName("10.0.0.1"), 8888) + val ipv6 = IPAddress(InetAddress.getByName("[2405:204:66a9:536c:873f:dc4a:f055:a298]"), 9737) + val ipv6LocalHost = IPAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735) val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) JsonSerializers.serialization.write(ipv4)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" - JsonSerializers.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" + JsonSerializers.serialization.write(ipv6)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[2405:204:66a9:536c:873f:dc4a:f055:a298]:9737"""" + JsonSerializers.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[::1]:9735"""" JsonSerializers.serialization.write(tor2)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" JsonSerializers.serialization.write(tor3)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" } @@ -167,21 +169,21 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { JsonSerializers.serialization.write(features)(JsonSerializers.formats) shouldBe """{"activated":{"initial_routing_sync":"optional","payment_secret":"mandatory","option_static_remotekey":"optional"},"unknown":[457,5000]}""" } - test("Payment Request") { + test("Bolt 11 invoice") { val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}""" } - test("Payment Request with routing hints") { + test("Bolt 11 invoice with routing hints") { val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}""" } - test("Payment Request with metadata") { + test("Bolt 11 invoice with metadata") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}""" } @@ -200,7 +202,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { toLocal = Map(ByteVector32(hex"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247") -> Btc(0.1))) )) ) - JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForFundingLocked":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" + JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForChannelReady":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" } test("type hints") { @@ -274,11 +276,11 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val dataNegotiating = hex"0100240000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000928544434bbbda1da0790cf138ef4b3881f5cec34b933ab77ffd57a7e12992bf780000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600140cbd801be794f9854b38981ee859e3a000ad104c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e032f5bf3637d4efa39b50a7bad5e3f6d32663a1cc207a166155f00f8b9a9f621cc026b8899cafac94bcfee408aecfec60e86a30082b0ea587d8a7ab6b2b309fc66110210996c2725e4129dad191f6c6d9ba8c35ab58df4d340051d7025d366423aa15703bf59f021a7431277a31a53b2101bd3ff5730adefeae596020e58c9099728b7f000000003229a820000000000000000000000000009c4000000002faf0800000000000bebc2002424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552aefd01bc0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000c089a180044a010000000000002200207ba66732d8b8e8863cbdcad193c4602b54df24f9af202a977e3a6315e85072704a01000000000000220020f25aee697436b14ebdc0bdab0b3a0c339c98e49cd6a8b84fbfb84c7c5557aad1400d030000000000220020a659aeaecad3be965ed5a498adac881967d2d3ed2a773019da1b26337c7d0d1372270c000000000022002095c2df1035ebb714a728a166d88332e1569b3da5c035037df1bc07e227b7e1ec040047304402205837856dc4bc8f4a679015460580c38a29899986f6cca09afe3525097dcef36702201b2abd6fdbfa890e247cbe6a096bb01f3d47901a552f7f5e5c6ecb6c80a07d1701483045022100abfa35999608ea0c993418f7d432c7f9710b993f4eb45e17fbb95e10c6899e190220542593d102382a92a11eac65bef87a75742ee4447abebb38fc47c4d27d74f9a2014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae68075c20000000000000000000000000000009c4000000000bebc200000000002faf080054b02aad4844030f2e1c3c61f95b34df8d14c0fdbddbe5d56090667016ca8979033d0000a1e29b94b3517a04106dc7bb74f4f091a2ee419d889ecf8434bcfdf127000000000000000000000000000000000000000000000000000000000000ff03228da9a93e02211af77afa576b94f57d747099099f5352298d8b3c4dbc7215c62424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000024d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d53824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5001600140cbd801be794f9854b38981ee859e3a000ad104c3824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50016001458043e6e40996eb9d3c77752a81398115ec43d5900010002db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59ac1a0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001a54fa574848f29f143c52f2717510e2a8831afaf7752217ce3452c7e88d24eb14905191e04229fc6433aef69bc33f9f2f6eb3c841fee628f7c583b9efe5a149aa47db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59f81d0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001708fc4039310f093081478d723e70b6bb1f17aa925472ccef2f79057aafce602726039f88ed9a0eb0841c61c39a5c224516288a81a53256693588f5af97e9e76c88fffd014e0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d5942210c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c0400483045022100ced76c39d6c63a52374b17103eba46ac6d21b5f5e427d48ffcd5e505a26477b90220588f30b94c0938632bcee8cfe9bc185325b2edde88c85fb4d226eda215abf26b01473044022042741816de3767e7b54fc1cdf94b6f0072203daf76c01ba5abb120801965413b02205237d5f3978b2332aa14ce65501fa784f51ea4e97030d44536f52f188f768074014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000000" val dataClosingLocal = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000948d8efaf118cae9f142433a624b41a9ef7e09327fa45083ebd7694e1a61c429980000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001401396601573e7aef81f91a89fc4b4b56feb7a35e0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc02fc17b2289973a29fd374ee24671d5d9643465a11bd725bac1d640b56493bd8f702b5460ae3379bc8a92d2aa2488f3e55a73d8909695e677beddd61a94d3655490f0357a1a37c3ae33b2a94bca0b6ae541036c22feeca86860c7c5501153ccfffd585028fb43c64d53c5d891bb0af3b7ed41acf3ebb0de59599b3e7836a8401645cb6e000000003028a82000000000000000007000500fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd987100fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3c00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9d00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4e00002710000000000598cd44000000002faf0800240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52aefd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae340169200004000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000002b204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c111b06004093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd7a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f5401f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a2650d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a0000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c010000002ba8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf658474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c4088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff708e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b0600404ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed47930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a402c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e2bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f11c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df404f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b082a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a00000000000000070005fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9dfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4efffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd9871fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3cfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00002710000000002faf0800000000000598cd446da1a38c05425e396afcb5fbb37ea2a9db15c56121e2fae11ec9f06da63917610371ece811f4d34713b42d65c38a775487c71c193a52df6ac46d6a013164681dd200000000000000000000000000000000000000080000000000000000000500000000000000050003d6ec06b2431d4e1f9c36a610af90d6c40000000000000006000317def351ac5745148ba3275aacb2b68f000000000000000700035d913d0e286346e1a50fe900f618b18600000000000000030003c35487d4d1e24e38bb4f923885d19b44000000000000000400030c7e6ecdbccf41baab2f7d95ca9e5e9bff03d32570a2783f1792d20fbe141f337d5dded9509c875e13288fe140400f36560e240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae0003003e0000fffffffffffc008030b0e83d8813791baf507a3c85c55d81cf80a50fd9f08504260743bb8033212800fc0003ffffffffffe80101460f774080d0e1bad18203f9af025451bfb4b8de611116890dd1fa45dd0f8e9802000007ffffffffffc80104c31735de0c70dc61744d1123646e49d871b662ae090c568a3ba7666fe370ce0c0003ffffffffffe40a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000061a8000000000fffd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae34016920ffed02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c04000000009000000001c62401000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c787b74c895285a11e04a680a50cef980d74a1ea53998bc2abaa2f7e13b6131b02204e0a8216f2efe89880feac989f9b0cfbf6aea1a61444d453d5547060aa65cc5901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000004fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402201f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a26502200d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a00148304502210093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd02207a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f501008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868111b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050048304502210088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff7022008e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d014730440220774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf6580220474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017902000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402202c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e02202bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b0147304402204ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed022047930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402204f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b0802202a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a01483045022100ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f102201c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b06000004ec02000000000101ea542e3a509a29b8d2afac84f21bc8f4e100df901b35c9fc19a331c588683519000000000090000000015c2100000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220359f29d38879b19665015a2b85bd60e4e3949292f75d2294d5a545aa392dd1fd022036aa2b42bdedd9deca319eece3e8a5670fe89e78231e1025b9fc1079a931398d01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101374c80d9d5213e7bf0ea94c3a8085866a4d533590d89f68e829162d03d431a2600000000009000000001e43400000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100d486e826ecfddcc474b719cb69e641efe97b4795b8ea4bb9729237788a3d35ed022051b097591bfc53dc855303672e17ad8488de9f2bcca670995060f331a7dfe55e01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101a436af3ff69624fd11b543c0b8d451190bcb7891a7259832cb64445571dc3003000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c203f2093d4b182ba5b78a471f806acf13c4d44f7191068abaf2b7a6742f427e02200fabe2a24e947f467a48fca7bba3ed68387ce46c32533366d99a52cefb0e3ad801004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ec020000000001013798507505fc7de865bccaad655dc03b3899bba86e139dfbc543a2a2f9b4b842000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220724b0e5a340e80de26f3908ae510a34c93b5632dbd539c1a155856cad5375ac402203de9139fbaadd86a11168c612b75f974a6143e68c564de77c8b350f2978ff2f901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000000000000" - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataNormal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NORMAL"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataWaitForFundingConfirmed.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_WAIT_FOR_FUNDING_CONFIRMED"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataShutdown.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_SHUTDOWN"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataNegotiating.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NEGOTIATING"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataClosingLocal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_CLOSING"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataNormal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NORMAL"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataWaitForFundingConfirmed.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_WAIT_FOR_FUNDING_CONFIRMED"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataShutdown.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_SHUTDOWN"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataNegotiating.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NEGOTIATING"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataClosingLocal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_CLOSING"""")) } @@ -304,6 +306,24 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { JsonSerializers.serialization.write(map)(JsonSerializers.formats) shouldBe s"""{"e2fc57221cfb1942224082174022f3f70a32005aa209956f9c94c6903f7669ff":"ok","8e3ec6e16436b7dc61b86340192603d05f16d4f8e06c8aaa02fbe2ad63209af3":"cannot execute command=CMD_UPDATE_RELAY_FEE in state=CLOSING","74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8":"channel 74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8 not found"}""" } + test("serialize short ids") { + val testCases = Map( + ShortIds(real = RealScidStatus.Unknown, localAlias = Alias(0x4455), remoteAlias_opt = Some(Alias(0x88888888L))) -> + """{"real":{"status":"unknown"},"localAlias":"0x4455","remoteAlias":"0x88888888"}""", + ShortIds(real = RealScidStatus.Temporary(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"temporary","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ShortIds(real = RealScidStatus.Final(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"final","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ) + for ((obj, json) <- testCases) { + JsonSerializers.serialization.write(obj)(JsonSerializers.formats) shouldBe json + } + } + + test("serialize block height") { + JsonSerializers.serialization.write(BlockHeight(123456))(JsonSerializers.formats) shouldBe "123456" + } + /** utility method that strips line breaks in the expected json */ def assertJsonEquals(actual: String, expected: String) = { val cleanedExpected = expected diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala index 6ed950f7e5..84f9968245 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.message -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.PacketAndSecrets import fr.acinq.eclair.message.OnionMessages._ @@ -40,7 +40,7 @@ class OnionMessagesSpec extends AnyFunSuite { val sessionKey = randomKey() val blindingSecret = randomKey() val destination = randomKey() - val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Left(Recipient(destination.publicKey, None)), Nil) + val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Recipient(destination.publicKey, None), Nil) assert(nextNodeId == destination.publicKey) process(destination, message) match { @@ -108,9 +108,9 @@ class OnionMessagesSpec extends AnyFunSuite { /* * Building the onion with functions from `OnionMessages` */ - val replyPath = buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Left(Recipient(dave.publicKey, pathId = Some(hex"01234567")))) + val replyPath = buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Recipient(dave.publicKey, pathId = Some(hex"01234567"))) assert(replyPath == routeFromCarol) - val (_, message) = buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, Right(replyPath), Nil) + val (_, message) = buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, BlindedPath(replyPath), Nil) assert(message == onionForAlice) /* @@ -223,12 +223,12 @@ class OnionMessagesSpec extends AnyFunSuite { val blindingSecret = randomKey() val blindingOverride = randomKey() val destination = randomKey() - val replyPath = buildRoute(blindingOverride, IntermediateNode(destination.publicKey) :: Nil, Left(Recipient(destination.publicKey, pathId = Some(hex"01234567")))) - assert(replyPath.blindingKey === blindingOverride.publicKey) - assert(replyPath.introductionNodeId === destination.publicKey) - val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Right(replyPath), Nil) - assert(nextNodeId === destination.publicKey) - assert(message.blindingKey === blindingOverride.publicKey) // blindingSecret was not used as the replyPath was used as is + val replyPath = buildRoute(blindingOverride, IntermediateNode(destination.publicKey) :: Nil, Recipient(destination.publicKey, pathId = Some(hex"01234567"))) + assert(replyPath.blindingKey == blindingOverride.publicKey) + assert(replyPath.introductionNodeId == destination.publicKey) + val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, BlindedPath(replyPath), Nil) + assert(nextNodeId == destination.publicKey) + assert(message.blindingKey == blindingOverride.publicKey) // blindingSecret was not used as the replyPath was used as is process(destination, message) match { case ReceiveMessage(_, Some(pathId)) if pathId == hex"01234567" => () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index ec781008c0..5067290b59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -55,20 +55,20 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) val payload = FinalPayload(TlvStream(EncryptedData(replyPath.encryptedPayloads.last) :: Nil, GenericTlv(UInt64(42), hex"abcd") :: Nil)) - postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) - messageRecipient.expectMessage(Response(payload)) + messageRecipient.expectMessage(Response(payload, pathId)) messageRecipient.expectNoMessage() } @@ -78,17 +78,18 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) - postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Disconnected(messageId)) messageRecipient.expectMessage(SendingStatus(Disconnected(messageId))) + messageRecipient.expectNoMessage() } test("timeout") { f => @@ -97,15 +98,15 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) val payload = FinalPayload(TlvStream(EncryptedData(replyPath.encryptedPayloads.last) :: Nil, GenericTlv(UInt64(42), hex"abcd") :: Nil)) postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) messageRecipient.expectMessage(NoReply) @@ -119,15 +120,16 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ val recipient = randomKey().publicKey - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), Nil) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), Nil) - postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) messageRecipient.expectMessage(SendingStatus(Sent(messageId))) + messageRecipient.expectNoMessage() } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala new file mode 100644 index 0000000000..657f0c25bf --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -0,0 +1,634 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment + +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} +import fr.acinq.eclair.payment.Bolt11Invoice._ +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} +import org.scalatest.TryValues.convertTryToSuccessOrFailure +import org.scalatest.funsuite.AnyFunSuite +import scodec.DecodeResult +import scodec.bits._ +import scodec.codecs.bits + +import scala.concurrent.duration.DurationInt +import scala.util.Success + +/** + * Created by fabrice on 15/05/17. + */ + +class Bolt11InvoiceSpec extends AnyFunSuite { + + val priv = PrivateKey(hex"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734") + val pub = priv.publicKey + val nodeId = pub + assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + + // Copy of Bolt11Invoice.apply that doesn't strip unknown features + def createInvoiceUnsafe(chainHash: ByteVector32, + amount: Option[MilliSatoshi], + paymentHash: ByteVector32, + privateKey: PrivateKey, + description: Either[String, ByteVector32], + minFinalCltvExpiryDelta: CltvExpiryDelta, + fallbackAddress: Option[String] = None, + expirySeconds: Option[Long] = None, + extraHops: List[List[ExtraHop]] = Nil, + timestamp: TimestampSecond = TimestampSecond.now(), + paymentSecret: ByteVector32 = randomBytes32(), + paymentMetadata: Option[ByteVector] = None, + features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = { + require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") + val prefix = prefixes(chainHash) + val tags = { + val defaultTags = List( + Some(PaymentHash(paymentHash)), + Some(description.fold(Description, DescriptionHash)), + Some(Bolt11Invoice.PaymentSecret(paymentSecret)), + paymentMetadata.map(Bolt11Invoice.PaymentMetadata), + fallbackAddress.map(FallbackAddress(_)), + expirySeconds.map(Expiry(_)), + Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), + Some(InvoiceFeatures(features)) + ).flatten + val routingInfoTags = extraHops.map(RoutingInfo) + defaultTags ++ routingInfoTags + } + Bolt11Invoice( + prefix = prefix, + amount_opt = amount, + createdAt = timestamp, + nodeId = privateKey.publicKey, + tags = tags, + signature = ByteVector.empty + ).sign(privateKey) + } + + test("check minimal unit is used") { + assert('p' == Amount.unit(1 msat)) + assert('p' == Amount.unit(99 msat)) + assert('n' == Amount.unit(100 msat)) + assert('p' == Amount.unit(101 msat)) + assert('n' == Amount.unit((1 sat).toMilliSatoshi)) + assert('u' == Amount.unit((100 sat).toMilliSatoshi)) + assert('n' == Amount.unit((101 sat).toMilliSatoshi)) + assert('u' == Amount.unit((1155400 sat).toMilliSatoshi)) + assert('m' == Amount.unit((1 millibtc).toMilliSatoshi)) + assert('m' == Amount.unit((10 millibtc).toMilliSatoshi)) + assert('m' == Amount.unit((1 btc).toMilliSatoshi)) + } + + test("decode empty amount") { + assert(Amount.decode("") == Success(None)) + assert(Amount.decode("0") == Success(None)) + assert(Amount.decode("0p") == Success(None)) + assert(Amount.decode("0n") == Success(None)) + assert(Amount.decode("0u") == Success(None)) + assert(Amount.decode("0m") == Success(None)) + } + + test("check that we can still decode non-minimal amount encoding") { + assert(Amount.decode("1000u") == Success(Some(100000000 msat))) + assert(Amount.decode("1000000n") == Success(Some(100000000 msat))) + assert(Amount.decode("1000000000p") == Success(Some(100000000 msat))) + } + + test("data string -> bitvector") { + assert(string2Bits("p") == bin"00001") + assert(string2Bits("pz") == bin"0000100010") + } + + test("minimal length long, left-padded to be multiple of 5") { + assert(long2bits(0) == bin"") + assert(long2bits(1) == bin"00001") + assert(long2bits(42) == bin"0000101010") + assert(long2bits(255) == bin"0011111111") + assert(long2bits(256) == bin"0100000000") + assert(long2bits(3600) == bin"000111000010000") + } + + test("verify that padding is zero") { + val codec = Bolt11Invoice.Codecs.alignedBytesCodec(bits) + assert(codec.decode(bin"1010101000").require == DecodeResult(bin"10101010", BitVector.empty)) + assert(codec.decode(bin"1010101001").isFailure) // non-zero padding + } + + test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") { + val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.isEmpty) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret.map(_.bytes).contains(hex"1111111111111111111111111111111111111111111111111111111111111111")) + assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("Please consider supporting this project")) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.tags.size == 4) + assert(invoice.sign(priv).toString == ref) + } + + test("Please send $3 for a cup of coffee to the same peer, within 1 minute") { + val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(250000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("1 cup coffee")) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") { + val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(250000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("ナンセンス 1杯")) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("Now send $24 for an entire list of things (hashed)") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.tags.size == 4) + assert(invoice.sign(priv).toString == ref) + } + + test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") { + val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lntb") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) + assert(invoice.routingInfo == List(List( + ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId.fromCoordinates("66051x263430x1800").success.value, 1 msat, 20, CltvExpiryDelta(3)), + ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId.fromCoordinates("197637x395016x2314").success.value, 2 msat, 30, CltvExpiryDelta(4)) + ))) + assert(invoice.tags.size == 6) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") { + val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress().contains("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(12)) + assert(invoice.tags.size == 6) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { + val refs = Seq( + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa", + // All upper-case + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".toUpperCase, + // With ignored fields + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x" + ) + + for (ref <- refs) { + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2500000000L msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("coffee beans")) + assert(features2bits(invoice.features) == bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.sign(priv).toString == ref.toLowerCase) + } + } + + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { + val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(2500000000L msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("coffee beans")) + assert(invoice.fallbackAddress().isEmpty) + assert(features2bits(invoice.features) == bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) + assert(!TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { + val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(967878534 msat)) + assert(invoice.paymentHash.bytes == hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.createdAt == TimestampSecond(1572468703L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.relativeExpiry == 604800.seconds) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(10)) + assert(invoice.routingInfo == Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId.fromCoordinates("589390x3312x1").success.value, 1000 msat, 2500, CltvExpiryDelta(40))))) + assert(invoice.sign(priv).toString == ref) + } + + test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { + val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.contains(1000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.description == Left("payment metadata inside")) + assert(invoice.paymentMetadata.contains(hex"01fafaf0")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) + } + + test("reject invalid invoices") { + val refs = Seq( + // Bech32 checksum is invalid. + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt", + // Malformed bech32 string (no 1). + "pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny", + // Malformed bech32 string (mixed case). + "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny", + // Signature is not recoverable. + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2", + // String is too short. + "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh", + // Invalid multiplier. + "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j", + // Invalid sub-millisatoshi precision. + "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" + ) + for (ref <- refs) { + assert(Bolt11Invoice.fromString(ref).isFailure) + } + } + + test("correctly serialize/deserialize variable-length tagged fields") { + val number = 123456 + val codec = Bolt11Invoice.Codecs.dataCodec(scodec.codecs.bits).as[Bolt11Invoice.Expiry] + val field = Bolt11Invoice.Expiry(number) + assert(field.toLong == number) + + val serializedExpiry = codec.encode(field).require + val field1 = codec.decodeValue(serializedExpiry).require + assert(field1 == field) + + val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(18)) + val serialized = invoice.toString + val Success(pr1) = Bolt11Invoice.fromString(serialized) + assert(invoice == pr1) + } + + test("ignore unknown tags") { + val invoice = Bolt11Invoice( + prefix = "lntb", + amount_opt = Some(100000 msat), + createdAt = TimestampSecond.now(), + nodeId = nodeId, + tags = List( + PaymentHash(ByteVector32(ByteVector.fill(32)(1))), + Description("description"), + UnknownTag21(BitVector("some data we don't understand".getBytes)) + ), + signature = ByteVector.empty).sign(priv) + + val serialized = invoice.toString + val Success(pr1) = Bolt11Invoice.fromString(serialized) + val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } + } + + test("ignore hash tags with invalid length") { + // Bolt11: A reader: MUST skip over p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively. + def bits(i: Int) = BitVector.fill(i * 5)(high = false) + + val inputs = Map( + "ppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(51)), + "pp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(53)), + "hpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(51)), + "hp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(53)), + "spnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(51)), + "sp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(53)), + "np5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(52)), + "npkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(54)) + ) + + for ((input, value) <- inputs) { + val data = string2Bits(input) + val decoded = Codecs.taggedFieldCodec.decode(data).require.value + assert(decoded == value) + val encoded = Codecs.taggedFieldCodec.encode(value).require + assert(encoded == data) + } + } + + test("accept uppercase invoices") { + val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j" + assert(Bolt11Invoice.fromString(input.toUpperCase()).get.toString == input) + } + + test("Pay 1 BTC without multiplier") { + val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" + val Success(invoice) = Bolt11Invoice.fromString(ref) + assert(invoice.amount_opt.contains(100000000000L msat)) + assert(features2bits(invoice.features) == BitVector.empty) + } + + test("supported invoice features") { + val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> Optional).toMap)) + case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule + val featureBits = Map( + Features(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) + Features(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), + Features(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) + ) + + for ((features, res) <- featureBits) { + val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) + assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) == res) + assert(Bolt11Invoice.fromString(invoice.toString).get == invoice) + } + } + + test("feature bits to minimally-encoded feature bytes") { + val testCases = Seq( + (bin" 0010000100000101", hex" 2105"), + (bin" 1010000100000101", hex" a105"), + (bin" 11000000000000110", hex"018006"), + (bin" 01000000000000110", hex" 8006"), + (bin" 001000000000000000", hex" 8000"), + (bin" 101000000000000000", hex"028000"), + (bin"0101010000000000110", hex"02a006"), + (bin"1000110000000000110", hex"046006") + ) + + for ((bitmask, featureBytes) <- testCases) { + assert(Features(bitmask).toByteVector == featureBytes) + } + } + + test("payment secret") { + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) + assert(invoice.paymentSecret.isDefined) + assert(invoice.features == Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) + + val Success(pr1) = Bolt11Invoice.fromString(invoice.toString) + assert(pr1.paymentSecret == invoice.paymentSecret) + + val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) + assert(pr2.paymentSecret.isEmpty) + + // An invoice that sets the payment secret feature bit must provide a payment secret. + assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) + + // A multi-part invoice must use a payment secret. + assertThrows[IllegalArgumentException]( + Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Optional, PaymentSecret -> Optional)) + ) + } + + test("trampoline") { + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) + + val pr1 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePaymentPrototype -> Optional)) + assert(!pr1.features.hasFeature(BasicMultiPartPayment)) + assert(pr1.features.hasFeature(TrampolinePaymentPrototype)) + + val pr2 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional)) + assert(pr2.features.hasFeature(BasicMultiPartPayment)) + assert(pr2.features.hasFeature(TrampolinePaymentPrototype)) + + val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + assert(!pr3.features.hasFeature(TrampolinePaymentPrototype)) + } + + test("nonreg") { + val requests = List( + "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah" -> PublicKey(hex"039f01ad62e5208940faff11d0bbc997582eafad7642aaf53de6a5f6551ab73400"), + "lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee" -> PublicKey(hex"03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda"), + "lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qrjgq7md5lu2hhkz657rs2a40xm2elaqda4krv6vy44my49x02azsqwr35puvgzjltd6dfth2awxcq49cx3srkl3zl34xhw7ppv840yf74wqq88rwr5" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qr3gqjdynggx20rz4nh98uknmtp2wkwk95zru8lfmw0cz9s3t0xpevuzpzz4k34cprpg9jfc3yp8zc827psug69j4w4pkn70rrfddcqf9wnqqcm2nc4" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9q9sqsgqruuf6y6hd77533p6ufl3dapzzt55uj7t88mgty7hvfpy5lzvntpyn82j72fr3wqz985lh7l2f5pnju66nman5z09p24qvp2k8443skqqq38n4w" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qxpqqsgqh88td9f8p8ls8r6devh9lhvppwqe6e0lkvehyu8ztu76m9s8nu2x0rfp5z9jmn2ta97mex2ne6yecvtz8r0qej62lvkngpaduhgytncqts4cxs" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + ) + + for ((req, nodeId) <- requests) { + val Success(invoice) = Bolt11Invoice.fromString(req) + assert(invoice.nodeId == nodeId) + assert(invoice.toString == req) + } + } + + test("no unknown feature in invoice") { + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) + assert(invoice.features == Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) + assert(Bolt11Invoice.fromString(invoice.toString).get == invoice) + } + + test("Invoices can't have high features") { + assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[Feature](Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242))))) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala new file mode 100644 index 0000000000..a8a4829bd3 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -0,0 +1,442 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment + +import fr.acinq.bitcoin.Bech32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt12Invoice.{hrp, signatureTag} +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec} +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferTypes, TlvStream} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64, randomKey} +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits._ + +import scala.concurrent.duration.DurationInt +import scala.util.Success + +class Bolt12InvoiceSpec extends AnyFunSuite { + + def signInvoiceTlvs(tlvs: TlvStream[InvoiceTlv], key: PrivateKey): TlvStream[InvoiceTlv] = { + val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key) + tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))) + } + + def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = { + val tlvs = OfferTypes.removeSignature(invoice.records) + val signedInvoice = Bolt12Invoice(signInvoiceTlvs(tlvs, key)) + assert(signedInvoice.checkSignature()) + signedInvoice + } + + test("check invoice signature") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) + assert(invoice.isValidFor(offer, request)) + assert(invoice.checkSignature()) + assert(!invoice.checkRefundSignature()) + assert(Bolt12Invoice.fromString(invoice.toString).get.toString == invoice.toString) + // changing signature makes check fail + val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown)) + assert(!withInvalidSignature.checkSignature()) + assert(!withInvalidSignature.isValidFor(offer, request)) + assert(!withInvalidSignature.checkRefundSignature()) + // changing fields makes the signature invalid + val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4")))) + assert(!withModifiedUnknownTlv.checkSignature()) + assert(!withModifiedUnknownTlv.isValidFor(offer, request)) + assert(!withModifiedUnknownTlv.checkRefundSignature()) + val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown)) + assert(!withModifiedAmount.checkSignature()) + assert(!withModifiedAmount.isValidFor(offer, request)) + assert(!withModifiedAmount.checkRefundSignature()) + } + + test("check that invoice matches offer") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) + assert(invoice.isValidFor(offer, request)) + assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request)) + // amount must match the offer + val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq)), nodeKey) + assert(!withOtherAmount.isValidFor(offer, request)) + // description must match the offer, may have appended info + val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq)), nodeKey) + assert(!withOtherDescription.isValidFor(offer, request)) + val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq)), nodeKey) + assert(withExtendedDescription.isValidFor(offer, request)) + // nodeId must match the offer + val otherNodeKey = randomKey() + val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq)), otherNodeKey) + assert(!withOtherNodeId.isValidFor(offer, request)) + // offerId must match the offer + val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq)), nodeKey) + assert(!withOtherOfferId.isValidFor(offer, request)) + // issuer must match the offer + val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob")))), nodeKey) + assert(!withOtherIssuer.isValidFor(offer, request)) + } + + test("check that invoice matches invoice request") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain) + val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain) + assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), randomBytes32()) + assert(invoice.isValidFor(offer, request)) + val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq)), nodeKey) + assert(!withInvalidFeatures.isValidFor(offer, request)) + val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq)), nodeKey) + assert(!withAmountTooBig.isValidFor(offer, request)) + val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2))), nodeKey) + assert(!withQuantity.isValidFor(offer, request)) + val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq)), nodeKey) + assert(!withOtherPayerKey.isValidFor(offer, request)) + val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman"))), nodeKey) + assert(!withPayerNote.isValidFor(offer, request)) + val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506"))), nodeKey) + assert(!withPayerInfo.isValidFor(offer, request)) + // Invoice request with more details about the payer. + val requestWithPayerDetails = { + val tlvs: Seq[InvoiceRequestTlv] = Seq( + OfferId(offer.offerId), + Amount(15000 msat), + PayerKey(payerKey.publicKey), + PayerInfo(hex"010203040506"), + PayerNote("I am Batman"), + FeaturesTlv(Features(VariableLengthOnion -> Mandatory)) + ) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) + assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails)) + assert(!withPayerDetails.isValidFor(offer, request)) + val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq)), nodeKey) + assert(!withOtherPayerInfo.isValidFor(offer, requestWithPayerDetails)) + assert(!withOtherPayerInfo.isValidFor(offer, request)) + val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq)), nodeKey) + assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails)) + assert(!withOtherPayerNote.isValidFor(offer, request)) + } + + test("check invoice expiry") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) + assert(!invoice.isExpired()) + assert(invoice.isValidFor(offer, request)) + val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x })), nodeKey) + assert(expiredInvoice1.isExpired()) + assert(!expiredInvoice1.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well + val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800)))), nodeKey) + assert(expiredInvoice2.isExpired()) + assert(!expiredInvoice2.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well + } + + test("check chain compatibility") { + val amount = 5000 msat + val (nodeKey, payerKey) = (randomKey(), randomKey()) + val (chain1, chain2) = (randomBytes32(), randomBytes32()) + val offerBtc = Offer(Some(amount), "bitcoin offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val requestBtc = InvoiceRequest(offerBtc, amount, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val invoiceImplicitBtc = { + val tlvs: Seq[InvoiceTlv] = Seq( + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + assert(invoiceImplicitBtc.isValidFor(offerBtc, requestBtc)) + val invoiceExplicitBtc = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(Block.LivenetGenesisBlock.hash), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + assert(invoiceExplicitBtc.isValidFor(offerBtc, requestBtc)) + val invoiceOtherChain = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain1), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc)) + val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey)))) + val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1) + val invoiceOtherChains = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain1), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerOtherChains.offerId), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), + Amount(amount), + Description(offerOtherChains.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + assert(invoiceOtherChains.isValidFor(offerOtherChains, requestOtherChains)) + val invoiceInvalidOtherChain = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain2), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerOtherChains.offerId), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), + Amount(amount), + Description(offerOtherChains.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) + } + assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains)) + val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true })), nodeKey) + assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains)) + } + + test("decode invalid invoice") { + val nodeKey = randomKey() + val tlvs = Seq[InvoiceTlv]( + Amount(765432 msat), + Description("minimal invoice"), + NodeId(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), randomKey().publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, Features.empty))), + CreatedAt(TimestampSecond(123456789L)), + PaymentHash(randomBytes32()), + ) + // This minimal invoice is valid. + val signed = signInvoiceTlvs(TlvStream[InvoiceTlv](tlvs), nodeKey) + val signedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(signed).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(signedEncoded).isSuccess) + // But removing any TLV makes it invalid. + for (tlv <- tlvs){ + val incomplete = tlvs.filterNot(_ == tlv) + val incompleteSigned = signInvoiceTlvs(TlvStream[InvoiceTlv](incomplete), nodeKey) + val incompleteSignedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(incompleteSigned).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(incompleteSignedEncoded).isFailure) + } + // Missing signature is also invalid. + val unsignedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(TlvStream[InvoiceTlv](tlvs)).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(unsignedEncoded).isFailure) + } + + test("encode/decode invoice with many fields") { + val chain = Block.TestnetGenesisBlock.hash + val offerId = ByteVector32.fromValidHex("8bc5978de5d625c90136dfa896a8a02cef33c5457027684687e3f98e0cfca4f0") + val amount = 123456 msat + val description = "invoice with many fields" + val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory) + val issuer = "alice" + val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") + val path = Sphinx.RouteBlinding.createDirect(PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), nodeKey.publicKey, hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb") + val payInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, Features.empty) + val quantity = 57 + val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65") + val payerNote = "I'm Bob" + val payerInfo = hex"a9eb6e526eac59cd9b89fb20" + val createdAt = TimestampSecond(1654654654L) + val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341") + val relativeExpiry = 3600 + val cltv = CltvExpiryDelta(123) + val fallbacks = Seq(FallbackAddress(4, hex"123d56f8"), FallbackAddress(6, hex"eb3adc68945ef601")) + val replaceInvoice = ByteVector32.fromValidHex("71ad033e5f42068225608770fa7672505449425db543a1f9c23bf03657aa37c1") + val tlvs = TlvStream[InvoiceTlv](Seq( + Chain(chain), + OfferId(offerId), + Amount(amount), + Description(description), + FeaturesTlv(features), + Issuer(issuer), + NodeId(nodeKey.publicKey), + Paths(Seq(path)), + PaymentPathsInfo(Seq(payInfo)), + Quantity(quantity), + PayerKey(payerKey), + PayerNote(payerNote), + PayerInfo(payerInfo), + CreatedAt(createdAt), + PaymentHash(paymentHash), + RelativeExpiry(relativeExpiry), + Cltv(cltv), + Fallbacks(fallbacks), + ReplaceInvoice(replaceInvoice) + ), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) + val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey) + val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature)))) + assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsqyycq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmcq5ecw92k3597h7kdndc64mg2xt709acf2gmxnnag5kq9a6wslznscqsyu5p4eckl7m69k0qpcppkpz3lq4chus9szjkgw9w7mgeknz7m7fpqqe02qmqdj08z62mz0jws0gxt45fyq8udel9jg5gd6xlgdrkdt5qywp0jsc93kcksk4x4yvk7s3dej984yh3y8sqqqyjjqqqqt7sz3qqqqqqqqqqq05qqqqqqqqqrcjqqqqqqqq5q4skc6trv50zzq7yvulrvrswhs5cervjm8jldxkpwqwru7ukm8suq59x36qrg5thhssqzwfxyz864ht3k8mck93xtedsvxua9wygjyqjm3ad8p3xa6429gn3v9dx2fc8fynk6gzzda3zsprz5qrtu23q2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dqjcqswzqhqyqrmxq2qwpqqqsfr64hcpvrqqz8t8twx39z77cqnyr9fadh9ym4vt8xehz0myquzquddqvl97ssxsgjkppmslfm8y5z5f9p9md2r58uuywlsxet65d7p7pqgrnx80xlmayz6d2kfupl65rk4mh07a6qllgs6cn0qgdqtpzyesh52h62dpegw6a6v2d38lr3jx07yasxr943wq6kuwa9gkuwq6279tl7szdcrqypq8lgp8yqq") + val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString) + assert(codedDecoded.chain == chain) + assert(codedDecoded.offerId.contains(offerId)) + assert(codedDecoded.amount == amount) + assert(codedDecoded.description == Left(description)) + assert(codedDecoded.features == features) + assert(codedDecoded.issuer.contains(issuer)) + assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1)) + assert(codedDecoded.blindedPaths == Seq(path)) + assert(codedDecoded.quantity.contains(quantity)) + assert(codedDecoded.payerKey.contains(payerKey)) + assert(codedDecoded.payerNote.contains(payerNote)) + assert(codedDecoded.payerInfo.contains(payerInfo)) + assert(codedDecoded.createdAt == createdAt) + assert(codedDecoded.paymentHash == paymentHash) + assert(codedDecoded.relativeExpiry == relativeExpiry.seconds) + assert(codedDecoded.minFinalCltvExpiryDelta == cltv) + assert(codedDecoded.fallbacks.contains(fallbacks)) + assert(codedDecoded.replaceInvoice.contains(replaceInvoice)) + assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) + } + + test("minimal tip") { + val nodeKey = PrivateKey(hex"48c6e5fcf499f50436f54c3b3edecdb0cb5961ca29d74bea5ab764828f08bf47") + assert(nodeKey.publicKey == PublicKey(hex"024ff5317f051c7f6eac0266c5cceaeb6c5775a940fab9854e47bfebf6bc7a0407")) + val payerKey = PrivateKey(hex"d817e8896c67d0bcabfdb93da7eb7fc698c829a181f994dd0ad866a8eda745e8") + assert(payerKey.publicKey == PublicKey(hex"031ef4439f638914de79220483dda32dfb7a431e799a5ce5a7643fbd70b2118e4e")) + val preimage = ByteVector32(hex"317d1fd8fec5f3ea23044983c2ba2a8043395b2a0790a815c9b12719aa5f1516") + val offer = Offer(None, "minimal tip", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq83pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 12000000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqf3qrm6y88mr3y2du7fzqjpamgedldayx8nenfwwtfmy877hpvs33e80qszhudm9rdk99qpnzktv6emdwq3gda2l77c6av7nn542sl3uhzq5yau26508s7n0mf3ztpnwr6f8vlxhjrlhc34w6sehs9jwydpxhxnws" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, hex"") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqzstd45ku6tdv9kzqarfwqg8sqj075ch7pgu0ah2cqnxchxw46mv2a66js86hxz5u3ala0mtc7syqupyrp8cxgf355un0gqnfhgqtnde9xqaazd4fcsrd5khlm4rns5v4aqpqd7aj6ja7pmk34s4llfjeju5y2lkuegn2dvjvnktkqvkxz05qvdjgqqjhfax8gmt6fgfunnhj99m3xk60auwvys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpdcmqqqqqqqqrcssynl4x9ls28rld6kqyek9en4wkmzhwk55p74es48y00lt76785pq8ycspaazrna3cj9x70y3qfq7a5vklk7jrreue5h895ajrl0tskggcun3gq33ds9tg9gsgtlj8vuulq6aca20u59mx5xzdg84pksgxgnahfamrsnnup8ck4lpwqgqpduzqrvfdar6jwu6c66w93stjn3qute7tgser2geyyadl0p5u0t4p08523c2tk9jlj9l2j5q47qdpta0rqtndwwdsqdfajpskxkrfrcqk4jc" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } + + test("minimal offer") { + val nodeKey = PrivateKey(hex"3b7a19e8320bb86431cf92cd7c69cc1dc0181c37d5a09875e4603c4e37d3705d") + assert(nodeKey.publicKey == PublicKey(hex"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5")) + val payerKey = PrivateKey(hex"0e00a9ef505292f90a0e8a7aa99d31750e885c42a3ef8866dd2bf97919aa3891") + assert(payerKey.publicKey == PublicKey(hex"033e94f2afd568d128f02ece844ad4a0a1ddf2a4e3a08beb2dba11b3f1134b0517")) + val preimage = ByteVector32(hex"09ad5e952ec39d45461ebdeceac206fb45574ae9054b5a454dd02c65f5ba1b7c") + val offer = Offer(Some(456000000 msat), "minimal offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wg0zzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 456001234 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd53xyqlffu40645dz28s9m8ggjk55zsamu4yuwsgh6edhggm8ugnfvz30uzqjq9tsnv60r570yqfypx3jghrff92qlcjwff0azwatsuehd0vkxxvz2wx07qlurz42ca0r96x6a4xh5h9gpz39w4em3687k6n3w9349g" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, hex"747e01a7152169b058a1fbc0024c254077db7e399308483e0c30e2352ba1d6cc") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd5s2p4kkjmnfd4skcgr0venx2ussnqpufzkf0cyl8ja6av6mq242d5rjk4mjdpq6xnf9j5s40jk2vzsu4agze6zapx6yyjyjxsahmqvurucz66plh2wsvum6el7ff0ugmghn4f7szqlfaudyqmqfvwk3q092y0n7n5zmajxp59t8dl6wpal5wwykj8fkrcqr9xe2umxlml4myefpzj9q207yn4zp23nfxkhmnu8mspu5lscnvgysc3rjnh8yzalyaw5nn6tqnn3emnu6wys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpktsx6gqqqqqqrcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4ycsra98j4l2k35fg7qhvapz26js2rh0j5n36pzlt9kaprvl3zd9s29egq33dsxf29gszev88kpfrveu8g5xr8khk6tev8jmpxg3pxfhpcx6f4jtlm4ltwgpwqgqpduzqpkmntgyk3ms0p5fe0ps22ed9v7zvzgwtvge2kl6ey7qhvz0s7u694fl75ushaga2syfvw46kuz7tjstwq4jgz52n599ph5nt0854hrq" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } + + test("offer with quantity") { + val nodeKey = PrivateKey(hex"334a488858f260a2bb262493f6edcd35470f110bba62c7a5f90c78a047b364df") + assert(nodeKey.publicKey == PublicKey(hex"0327afd599da3226f4608b96ab042fe558bf558211d3c5e67ecc8be9963220434f")) + val payerKey = PrivateKey(hex"4b4129a801ea631e25903cd59dd7f7a6820c19d73aa0b095496e21027934becf") + assert(payerKey.publicKey == PublicKey(hex"027c6d03fa8f366e2ef8017cdfaf5d3cf1a3b0123db1318263b662c0aa9ec9c959")) + val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8") + val offer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(100000 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(50), + QuantityMax(1000), + NodeId(nodeKey.publicKey))) + val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezsgkzmrfvdj5qcnfvaeksmms9e3k7mgkqyepsqsraq0zzqe84l2enk3jym6xpzuk4vzzle2cha2cyywnchn8anytaxtrygzrfu" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 7200000 msat, 72, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + val encodedRequest = "lnr1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqgqpfqnzqlrdq0ag7dnw9muqzlxl4awneudrkqfrmvf3sf3mvckq420vnj2e7pq998np5gs3khqdqpgztenk5k5wqhzlxjg0ed4q9439yh8dayzz7q24kay7qsrhxg8tf303g223fknj8d79d3dvj78nlkg9s8c5hyqgz5" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(34), Features.empty, hex"9134d86e269a13203bd85bb3fd05bf396b72fcb9fd5206e3a392f6a0ab94011d") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqzsndanxvetjypmkjargypch2ctww35hg7gsnqpj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxncrjczxkxg06n3zvk280qntncsgx4kfducc57dy3cj0qcprf9sgnqfqzq3pntdcen5g5nuy84ny8e9geerst688u82xmyenwhuy5dju9er3rsqryttw2zsv5xn6sht2lvakdved7thlw4ggk9g3ga9qpc6rlh2xhwhjkt3rnlcz03szw6ww2khdx3rv4m0x2ys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmwaqqqqqqqqzsgkzmrfvdj5qcnfvaeksmms9e3k7mg7yypj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxneqq9yzvgrud5pl4rekdch0sqtum7h460835wcpy0d3xxpx8dnzcz4fajwfty5qgckcypdj5gr7q6pnms32xvy3rx08aw5zjpkua90zymcv947urlhz72wg5g7xkqhqyqpz7pqrs69fh5xv6gvfxahya334fc6ycjly4lzqzap5w543qrpx42pjn5ez8mtx73k0vlvhjjskspmjyukdqugwy54zf9qxxss3hh7q2uxlcg" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 697d2789d9..70006e388a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -19,20 +19,20 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.Status.Failure import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.db.IncomingPaymentStatus +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.PaymentReceived.PartialPayment -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.{DoFulfill, GetPendingPayments, PendingPayments, ReceivePayment} import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -46,18 +46,18 @@ import scala.concurrent.duration._ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { - val featuresWithoutMpp = Features( + val featuresWithoutMpp = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, ) - val featuresWithMpp = Features( + val featuresWithMpp = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional ) - val featuresWithKeySend = Features( + val featuresWithKeySend = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, KeySend -> Optional @@ -75,7 +75,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val register = TestProbe() val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) - withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeParams.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight), register, eventListener, TestProbe()))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeParams.channelConf.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight), register, eventListener, TestProbe()))) } } @@ -86,62 +86,62 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { sender.send(handlerWithoutMpp, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - val incoming = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val invoice = sender.expectMsgType[Invoice] + val incoming = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.isDefined) - assert(incoming.get.status === IncomingPaymentStatus.Pending) - assert(!incoming.get.paymentRequest.isExpired) - assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash) + assert(incoming.get.status == IncomingPaymentStatus.Pending) + assert(!incoming.get.invoice.isExpired()) + assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } { sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("another coffee with multi-part"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } { sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("bad expiry"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + val invoice = sender.expectMsgType[Invoice] + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) eventListener.expectNoMessage(100 milliseconds) sender.expectNoMessage(50 millis) } } - test("Payment request generation should fail when the amount is not valid") { f => + test("Invoice generation should fail when the amount is not valid") { f => import f._ // negative amount should fail @@ -156,66 +156,66 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // success with 1 mBTC sender.send(handlerWithMpp, ReceivePayment(Some(100000000 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.contains(100000000 msat) && pr.nodeId.toString == nodeParams.nodeId.toString) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.amount_opt.contains(100000000 msat) && invoice.nodeId.toString == nodeParams.nodeId.toString) } - test("Payment request generation should succeed when the amount is not set") { f => + test("Invoice generation should succeed when the amount is not set") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(None, Left("This is a donation PR"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.isEmpty && pr.nodeId.toString == Alice.nodeParams.nodeId.toString) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.amount_opt.isEmpty && invoice.nodeId.toString == Alice.nodeParams.nodeId.toString) } - test("Payment request generation should handle custom expiries or use the default otherwise") { f => + test("Invoice generation should handle custom expiries or use the default otherwise") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee"))) - val pr1 = sender.expectMsgType[PaymentRequest] - assert(pr1.minFinalCltvExpiryDelta === Some(nodeParams.minFinalExpiryDelta)) - assert(pr1.expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) + val pr1 = sender.expectMsgType[Invoice] + assert(pr1.minFinalCltvExpiryDelta == nodeParams.channelConf.minFinalExpiryDelta) + assert(pr1.relativeExpiry == Alice.nodeParams.invoiceExpiry) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with custom expiry"), expirySeconds_opt = Some(60))) - val pr2 = sender.expectMsgType[PaymentRequest] - assert(pr2.minFinalCltvExpiryDelta === Some(nodeParams.minFinalExpiryDelta)) - assert(pr2.expiry === Some(60)) + val pr2 = sender.expectMsgType[Invoice] + assert(pr2.minFinalCltvExpiryDelta == nodeParams.channelConf.minFinalExpiryDelta) + assert(pr2.relativeExpiry == 60.seconds) } - test("Payment request generation with trampoline support") { _ => + test("Invoice generation with trampoline support") { () => val sender = TestProbe() { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithoutMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } } - test("Generated payment request contains the provided extra hops") { f => + test("Generated invoice contains the provided extra hops") { f => import f._ val x = randomKey().publicKey @@ -227,127 +227,127 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val route_x_t = extraHop_x_t :: Nil sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with additional routing info"), extraHops = List(route_x_z, route_x_t))) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Seq(route_x_z, route_x_t)) + assert(sender.expectMsgType[Bolt11Invoice].routingInfo == Seq(route_x_z, route_x_t)) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee without routing info"))) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Nil) + assert(sender.expectMsgType[Bolt11Invoice].routingInfo == Nil) } - test("PaymentHandler should reject incoming payments if the payment request is expired") { f => + test("PaymentHandler should reject incoming payments if the invoice is expired") { f => import f._ sender.send(handlerWithoutMpp, ReceivePayment(Some(1000 msat), Left("some desc"), expirySeconds_opt = Some(0))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(pr.isExpired) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] - val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) + val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) } - test("PaymentHandler should reject incoming multi-part payment if the payment request is expired") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice is expired") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part expired"), expirySeconds_opt = Some(0))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.isExpired) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) + val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) } - test("PaymentHandler should reject incoming multi-part payment if the payment request does not allow it") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice does not allow it") { f => import f._ sender.send(handlerWithoutMpp, ReceivePayment(Some(1000 msat), Left("no multi-part support"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid expiry") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part invalid expiry"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an unknown payment hash") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part unknown payment hash"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too low") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part total amount too low"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too high") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part total amount too low"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid payment secret") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part invalid payment secret"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) // Invalid payment secret. - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.map(_.reverse), invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment timeout") { f => @@ -356,15 +356,15 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Partial payment missing additional parts. f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee"))) - val pr1 = f.sender.expectMsgType[PaymentRequest] + val pr1 = f.sender.expectMsgType[Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata))) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte"))) - val pr2 = f.sender.expectMsgType[PaymentRequest] + val pr2 = f.sender.expectMsgType[Invoice] val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata))) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -372,9 +372,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil - assert(commands.toSet === Set( - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)), - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true)) + assert(commands.toSet == Set( + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)), + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -383,11 +383,11 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be failed. f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), Some(PaymentTimeout))) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) - assert(incomingPayment.status === IncomingPaymentStatus.Pending) + assert(incomingPayment.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment success") { f => @@ -396,38 +396,38 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val preimage = randomBytes32() f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 fast coffee"), paymentPreimage_opt = Some(preimage))) - val pr = f.sender.expectMsgType[PaymentRequest] + val invoice = f.sender.expectMsgType[Invoice] - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // Invalid payment secret -> should be rejected. - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata))) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.map(_.reverse), invoice.paymentMetadata))) val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), - Register.Forward(ActorRef.noSender, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), - Register.Forward(ActorRef.noSender, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty }) // Extraneous HTLCs should be fulfilled. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, pr.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) - assert(f.eventListener.expectMsgType[PaymentReceived].amount === 200.msat) - val received2 = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1200.msat) + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) + assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) + val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat) f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -439,35 +439,35 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val preimage = randomBytes32() f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 coffee, no sugar"), paymentPreimage_opt = Some(preimage))) - val pr = f.sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.paymentHash == Crypto.sha256(preimage)) + val invoice = f.sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.paymentHash == Crypto.sha256(preimage)) - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty }) - val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) - val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) + val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, add2.channelId, CMD_FULFILL_HTLC(2, preimage, commit = true)), - Register.Forward(ActorRef.noSender, add3.channelId, CMD_FULFILL_HTLC(5, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.paymentHash === pr.paymentHash) - assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(paymentReceived.paymentHash == invoice.paymentHash) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -483,17 +483,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) } test("PaymentHandler should reject KeySend payment when feature is disabled") { f => @@ -505,55 +505,55 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) - f.register.expectMsg(Register.Forward(ActorRef.noSender, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) } - test("PaymentHandler should reject incoming payments if the payment request doesn't exist") { f => + test("PaymentHandler should reject incoming payments if the invoice doesn't exist") { f => import f._ val paymentHash = randomBytes32() val paymentSecret = randomBytes32() - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret, None))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, Some(paymentSecret), None))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } - test("PaymentHandler should reject incoming multi-part payment if the payment request doesn't exist") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice doesn't exist") { f => import f._ val paymentHash = randomBytes32() val paymentSecret = randomBytes32() - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, Some(paymentSecret), Some(hex"012345")))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } - test("PaymentHandler should fail fulfilling incoming payments if the payment request doesn't exist") { f => + test("PaymentHandler should fail fulfilling incoming payments if the invoice doesn't exist") { f => import f._ val paymentPreimage = randomBytes32() val paymentHash = Crypto.sha256(paymentPreimage) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val fulfill = DoFulfill(paymentPreimage, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index 5092339d1b..f2c2a79e40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ import fr.acinq.eclair.wire.protocol @@ -73,9 +73,9 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val fail = f.parent.expectMsgType[MultiPartPaymentFailed] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === protocol.PaymentTimeout) - assert(fail.parts.toSet === parts.toSet) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == protocol.PaymentTimeout) + assert(fail.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -85,14 +85,14 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { val f = createFixture(250 millis, 1000 msat) f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 150 msat, 1)) f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 100 msat, 2)) - assert(f.parent.expectMsgType[MultiPartPaymentFailed].parts.length === 2) + assert(f.parent.expectMsgType[MultiPartPaymentFailed].parts.length == 2) val extraPart = createMultiPartHtlc(1000 msat, 300 msat, 3) f.parent.send(f.handler, extraPart) val fail = f.parent.expectMsgType[ExtraPaymentReceived[PaymentPart]] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === Some(protocol.PaymentTimeout)) - assert(fail.payment === extraPart) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == Some(protocol.PaymentTimeout)) + assert(fail.payment == extraPart) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -103,9 +103,9 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 600 msat, 1)) f.parent.send(f.handler, createMultiPartHtlc(1100 msat, 650 msat, 2)) val fail = f.parent.expectMsgType[MultiPartPaymentFailed] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === IncorrectOrUnknownPaymentDetails(1100 msat, f.currentBlockHeight)) - assert(fail.parts.length === 2) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == IncorrectOrUnknownPaymentDetails(1100 msat, f.currentBlockHeight)) + assert(fail.parts.length == 2) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -121,8 +121,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) } @@ -137,8 +137,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -150,7 +150,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, part) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts === Seq(part)) + assert(paymentResult.parts == Seq(part)) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -162,7 +162,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, part) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts === Seq(part)) + assert(paymentResult.parts == Seq(part)) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -177,14 +177,14 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { ) parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) val extra = createMultiPartHtlc(1000 msat, 300 msat, 3) f.parent.send(f.handler, extra) val extraneousPayment = f.parent.expectMsgType[ExtraPaymentReceived[PaymentPart]] - assert(extraneousPayment.paymentHash === paymentHash) - assert(extraneousPayment.payment === extra) + assert(extraneousPayment.paymentHash == paymentHash) + assert(extraneousPayment.payment == extra) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -203,8 +203,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, parts(1)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) } @@ -219,7 +219,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, parts(1)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index f04524ea93..df2e721385 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -18,19 +18,20 @@ package fr.acinq.eclair.payment import akka.actor.{ActorContext, ActorRef, Status} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus} +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._ import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentInitiator} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{Announcements, RouteNotFound} @@ -77,27 +78,27 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful first attempt (single part)") { f => import f._ - assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) + assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) - assert(payFsm.stateName === WAIT_FOR_ROUTES) + assert(payFsm.stateName == WAIT_FOR_ROUTES) val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(singleRoute))) val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute] - assert(childPayment.route === Right(singleRoute)) - assert(childPayment.finalPayload.expiry === expiry) - assert(childPayment.finalPayload.paymentSecret === payment.paymentSecret) - assert(childPayment.finalPayload.amount === finalAmount) - assert(childPayment.finalPayload.totalAmount === finalAmount) - assert(payFsm.stateName === PAYMENT_IN_PROGRESS) + assert(childPayment.route == Right(singleRoute)) + assert(childPayment.finalPayload.expiry == expiry) + assert(childPayment.finalPayload.paymentSecret == payment.paymentSecret) + assert(childPayment.finalPayload.amount == finalAmount) + assert(childPayment.finalPayload.totalAmount == finalAmount) + assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 1) - assert(result.amountWithFees === finalAmount + 100.msat) - assert(result.trampolineFees === 0.msat) - assert(result.nonTrampolineFees === 100.msat) + assert(result.amountWithFees == finalAmount + 100.msat) + assert(result.trampolineFees == 0.msat) + assert(result.nonTrampolineFees == 100.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -110,12 +111,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful first attempt (multiple parts)") { f => import f._ - assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) + assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) - assert(payFsm.stateName === WAIT_FOR_ROUTES) + assert(payFsm.stateName == WAIT_FOR_ROUTES) val routes = Seq( Route(500000 msat, hop_ab_1 :: hop_be :: Nil), @@ -123,18 +124,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS ) router.send(payFsm, RouteResponse(routes)) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil - assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet) - assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry)) - assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret)) - assert(childPayments.map(_.finalPayload.paymentMetadata).toSet === Set(Some(hex"012345"))) - assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat)) - assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat)) - assert(payFsm.stateName === PAYMENT_IN_PROGRESS) + assert(childPayments.map(_.route).toSet == routes.map(r => Right(r)).toSet) + assert(childPayments.map(_.finalPayload.expiry).toSet == Set(expiry)) + assert(childPayments.map(_.finalPayload.paymentSecret).toSet == Set(payment.paymentSecret)) + assert(childPayments.map(_.finalPayload.paymentMetadata).toSet == Set(Some(hex"012345"))) + assert(childPayments.map(_.finalPayload.amount).toSet == Set(500000 msat, 700000 msat)) + assert(childPayments.map(_.finalPayload.totalAmount).toSet == Set(1200000 msat)) + assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1200200.msat) - assert(result.trampolineFees === 200000.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1200200.msat) + assert(result.trampolineFees == 200000.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -150,18 +151,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // We include a bunch of additional tlv records. val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef") - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil childPayments.map(_.finalPayload.asInstanceOf[PaymentOnion.FinalTlvPayload]).foreach(p => { - assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion] === Some(trampolineTlv)) - assert(p.records.unknown.toSeq === Seq(userCustomTlv)) + assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion] == Some(trampolineTlv)) + assert(p.records.unknown.toSeq == Seq(userCustomTlv)) }) val result = fulfillPendingPayments(f, 2) - assert(result.trampolineFees === 1000.msat) + assert(result.trampolineFees == 1000.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -174,7 +175,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) @@ -192,9 +193,9 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(childId)) val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) - assert(result.trampolineFees === 0.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1000200.msat) + assert(result.trampolineFees == 0.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -207,7 +208,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry failures while waiting for routes") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil)))) @@ -219,7 +220,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // When we retry, we ignore the failing node and we let the router know about the remaining pending route. - router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee, ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) + router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) // The second part fails while we're still waiting for new routes. childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into @@ -235,8 +236,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1000200.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -249,7 +250,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry local channel failures") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -274,7 +275,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry without ignoring channels") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil)))) @@ -289,7 +290,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // of the amount to send. val expectedRouteRequest = RouteRequest( nodeParams.nodeId, e, - failedRoute.amount, maxFee - failedRoute.fee, + failedRoute.amount, maxFee - failedRoute.fee(false), ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab_1, a, b))), pendingPayments = Seq(pendingRoute), allowMultiPart = true, @@ -303,7 +304,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) + assert(result.amountWithFees == 1000200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -317,31 +318,32 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) + val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) + assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] childPayFsm.expectNoMessage(100 millis) // B changed his fees and expiry after the invoice was issued. - val channelUpdate = hop_be.lastUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) + val channelUpdate = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24))) + val updatedExtraEdge = router.expectMsgType[RouteRequest].extraEdges.head + assert(updatedExtraEdge == BasicEdge(b, e, hop_be.shortChannelId, channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, channelUpdate.cltvExpiryDelta)) } test("retry with ignored routing hints (temporary channel failure)") { f => import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) + val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) + assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] @@ -349,19 +351,20 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // B doesn't have enough liquidity on this channel. // NB: we need a channel update with a valid signature, otherwise we'll ignore the node instead of this specific channel. - val channelUpdate = Announcements.makeChannelUpdate(hop_be.lastUpdate.chainHash, priv_b, e, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.cltvExpiryDelta, hop_be.lastUpdate.htlcMinimumMsat, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.htlcMaximumMsat.get) + val channelUpdateBE = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate + val channelUpdateBE1 = Announcements.makeChannelUpdate(channelUpdateBE.chainHash, priv_b, e, channelUpdateBE.shortChannelId, channelUpdateBE.cltvExpiryDelta, channelUpdateBE.htlcMinimumMsat, channelUpdateBE.feeBaseMsat, channelUpdateBE.feeProportionalMillionths, channelUpdateBE.htlcMaximumMsat.get) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdate)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] - assert(routeRequest.assistedRoutes.head.head === routingHint) - assert(routeRequest.ignore.channels.map(_.shortChannelId) === Set(channelUpdate.shortChannelId)) + assert(routeRequest.extraEdges.head == extraEdge) + assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(channelUpdateBE1.shortChannelId)) } - test("update routing hints") { _ => - val routingHints = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24))), - Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + test("update routing hints") { () => + val extraEdges = Seq( + BasicEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), BasicEdge(b, c, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24)), + BasicEdge(a, c, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144)) ) def makeChannelUpdate(shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportional: Long, cltvExpiryDelta: CltvExpiryDelta): ChannelUpdate = { @@ -374,11 +377,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), UnreadableRemoteFailure(finalAmount, Nil) ) - val routingHints1 = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))), - Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + val extraEdges1 = Seq( + BasicEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), BasicEdge(b, c, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)), + BasicEdge(a, c, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144)) ) - assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(extraEdges1.zip(PaymentFailure.updateExtraEdges(failures, extraEdges)).forall{case (e1, e2) => e1 == e2}) } { val failures = Seq( @@ -387,18 +390,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))), ) - val routingHints1 = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), ExtraHop(b, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))), - Seq(ExtraHop(a, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))) + val extraEdges1 = Seq( + BasicEdge(a, b, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), BasicEdge(b, c, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)), + BasicEdge(a, c, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22)) ) - assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(extraEdges1.zip(PaymentFailure.updateExtraEdges(failures, extraEdges)).forall{case (e1, e2) => e1 == e2}) } } test("abort after too many failed attempts") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 2, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -429,19 +432,19 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ sender.watch(payFsm) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, Status.Failure(RouteNotFound)) val result = sender.expectMsgType[PaymentFailed] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.failures === Seq(LocalFailure(finalAmount, Nil, RouteNotFound))) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.failures == Seq(LocalFailure(finalAmount, Nil, RouteNotFound))) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(cfg.id) assert(outgoing.status.isInstanceOf[OutgoingPaymentStatus.Failed]) - assert(outgoing.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures === Seq(FailureSummary(FailureType.LOCAL, RouteNotFound.getMessage, Nil, None))) + assert(outgoing.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures == Seq(FailureSummary(FailureType.LOCAL, RouteNotFound.getMessage, Nil, None))) sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -459,7 +462,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if recipient sends error") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -467,7 +470,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) - assert(result.failures.length === 1) + assert(result.failures.length == 1) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "FAILURE") @@ -480,7 +483,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if payment gets settled on chain") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -488,13 +491,13 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.hops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) - assert(result.failures.length === 1) + assert(result.failures.length == 1) } test("abort if recipient sends error during retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -506,13 +509,13 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.expectMsgType[RouteRequest] val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) - assert(result.failures.length === 2) + assert(result.failures.length == 2) } test("receive partial success after retriable failure (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -525,14 +528,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val result = fulfillPendingPayments(f, 1) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments - assert(result.parts.length === 1 && result.parts.head.id === successId) + assert(result.nonTrampolineFees == successRoute.fee(false)) // we paid the fee for only one of the partial payments + assert(result.parts.length == 1 && result.parts.head.id == successId) } test("receive partial success after abort (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -541,20 +544,20 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) - awaitCond(payFsm.stateName === PAYMENT_ABORTED) + awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee, randomBytes32(), Some(successRoute.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.paymentPreimage === paymentPreimage) - assert(result.parts.length === 1 && result.parts.head.id === successId) - assert(result.recipientAmount === finalAmount) - assert(result.recipientNodeId === finalRecipient) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.paymentPreimage == paymentPreimage) + assert(result.parts.length == 1 && result.parts.head.id == successId) + assert(result.recipientAmount == finalAmount) + assert(result.recipientNodeId == finalRecipient) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees == successRoute.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -565,7 +568,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial failure after success (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -573,16 +576,16 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32(), Some(route.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) - awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED) + awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) val result = sender.expectMsgType[PaymentSent] - assert(result.parts.length === 1 && result.parts.head.id === childId) + assert(result.parts.length == 1 && result.parts.head.id == childId) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === route.fee) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees == route.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -595,20 +598,20 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS sender.watch(payFsm) val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending - assert(pending.size === childCount) + assert(pending.size == childCount) val partialPayments = pending.map { - case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32(), Some(route.hops)) + case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops)) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.paymentPreimage === paymentPreimage) - assert(result.parts.toSet === partialPayments.toSet) - assert(result.recipientAmount === finalAmount) - assert(result.recipientNodeId === finalRecipient) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.paymentPreimage == paymentPreimage) + assert(result.parts.toSet == partialPayments.toSet) + assert(result.recipientAmount == finalAmount) + assert(result.recipientNodeId == finalRecipient) sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -625,8 +628,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending childPayFsm.send(payFsm, childFailure) // this failure should trigger an abort if (pending.size > 1) { - awaitCond(payFsm.stateName === PAYMENT_ABORTED) - assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size === pending.size - 1) + awaitCond(payFsm.stateName == PAYMENT_ABORTED) + assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size == pending.size - 1) // Fail all remaining child payments. payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId => childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) @@ -634,8 +637,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS } val result = sender.expectMsgType[PaymentFailed] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) assert(result.failures.nonEmpty) sender.expectTerminated(payFsm) @@ -698,13 +701,13 @@ object MultiPartPaymentLifecycleSpec { val channelUpdate_ad = defaultChannelUpdate.copy(shortChannelId = channelId_ad) val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = channelId_de) - val hop_ab_1 = ChannelHop(a, b, channelUpdate_ab_1) - val hop_ab_2 = ChannelHop(a, b, channelUpdate_ab_2) - val hop_be = ChannelHop(b, e, channelUpdate_be) - val hop_ac_1 = ChannelHop(a, c, channelUpdate_ac_1) - val hop_ac_2 = ChannelHop(a, c, channelUpdate_ac_2) - val hop_ce = ChannelHop(c, e, channelUpdate_ce) - val hop_ad = ChannelHop(a, d, channelUpdate_ad) - val hop_de = ChannelHop(d, e, channelUpdate_de) + val hop_ab_1 = channelHopFromUpdate(a, b, channelUpdate_ab_1) + val hop_ab_2 = channelHopFromUpdate(a, b, channelUpdate_ab_2) + val hop_be = channelHopFromUpdate(b, e, channelUpdate_be) + val hop_ac_1 = channelHopFromUpdate(a, c, channelUpdate_ac_1) + val hop_ac_2 = channelHopFromUpdate(a, c, channelUpdate_ac_2) + val hop_ce = channelHopFromUpdate(c, e, channelUpdate_ce) + val hop_ad = channelHopFromUpdate(a, d, channelUpdate_ad) + val hop_de = channelHopFromUpdate(d, e, channelUpdate_de) } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index e387ba113b..e534d6ce61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -18,15 +18,15 @@ package fr.acinq.eclair.payment import akka.actor.{ActorContext, ActorRef} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.UInt64.Conversions._ -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.PaymentPacketSpec._ -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures @@ -37,10 +37,10 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax} +import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID import scala.concurrent.duration._ @@ -53,22 +53,22 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) - val featuresWithoutMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithoutMpp: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory ) - val featuresWithMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithMpp: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ) - val featuresWithTrampoline: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithTrampoline: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, - TrampolinePayment -> Optional, + TrampolinePaymentPrototype -> Optional, ) case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory { @@ -86,7 +86,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike override def withFixture(test: OneArgTest): Outcome = { val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp - val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(features.collect { case (f, s) => (f: Feature, s) })) + val nodeParams = TestConstants.Alice.nodeParams.copy(features = features.unscoped()) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) @@ -97,14 +97,14 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward payment with user custom tlv records") { f => import f._ val customRecords = Seq(GenericTlv(500L, hex"01020304"), GenericTlv(501L, hex"d34db33f")) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) - val req = SendPaymentToNode(finalAmount, pr, 1, Channel.MIN_CLTV_EXPIRY_DELTA, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) + val req = SendPaymentToNode(sender.ref, finalAmount, invoice, 1, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] val FinalTlvPayload(tlvs) = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload assert(tlvs.get[AmountToForward].get.amount == finalAmount) - assert(tlvs.get[OutgoingCltv].get.cltv == req.fallbackFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) + assert(tlvs.get[OutgoingCltv].get.cltv == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) assert(tlvs.unknown == customRecords) } @@ -124,66 +124,64 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("reject payment with unknown mandatory feature") { f => import f._ val taggedFields = List( - PaymentRequest.PaymentHash(paymentHash), - PaymentRequest.Description("Some invoice"), - PaymentRequest.PaymentSecret(randomBytes32()), - PaymentRequest.Expiry(3600), - PaymentRequest.PaymentRequestFeatures(bin"000001000000000000000000000000000100000100000000") // feature 42 + Bolt11Invoice.PaymentHash(paymentHash), + Bolt11Invoice.Description("Some invoice"), + Bolt11Invoice.PaymentSecret(randomBytes32()), + Bolt11Invoice.Expiry(3600), + Bolt11Invoice.InvoiceFeatures(Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), unknown = Set(UnknownFeature(42)))) ) - val pr = PaymentRequest("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) - val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) + val req = SendPaymentToNode(sender.ref, finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) + assert(fail.id == id) assert(fail.failures.head.isInstanceOf[LocalFailure]) - assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(pr.features.features)) + assert(fail.failures.head.asInstanceOf[LocalFailure].t == UnsupportedFeatures(invoice.features)) } test("forward payment with pre-defined route") { f => import f._ - // we prioritize the invoice's finalExpiryDelta over the one from SendPaymentToRouteRequest - val ignoredFinalExpiryDelta = CltvExpiryDelta(18) val finalExpiryDelta = CltvExpiryDelta(36) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) val route = PredefinedNodeRoute(Seq(a, b, c)) - val request = SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get, pr.paymentMetadata))) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret, invoice.paymentMetadata))) sender.send(initiator, GetPayment(Left(payment.paymentId))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, request))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, request))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) - val pf = PaymentFailed(payment.paymentId, pr.paymentHash, Nil) + val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(NoPendingPayment(Right(pr.paymentHash))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(NoPendingPayment(Right(invoice.paymentHash))) } test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f => import f._ val finalExpiryDelta = CltvExpiryDelta(24) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(featuresWithMpp)) - val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = featuresWithMpp) + val req = SendPaymentToNode(sender.ref, finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, FinalTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, FinalTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val pf = PaymentFailed(id, pr.paymentHash, Nil) + val pf = PaymentFailed(id, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) @@ -194,19 +192,19 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp)) - val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) + val req = SendPaymentToNode(sender.ref, finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, pr.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.paymentSecret, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, invoice.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, pr.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) + val ps = PaymentSent(id, invoice.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) @@ -217,121 +215,121 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId)) - val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] - assert(msg.replyTo === initiator) - assert(msg.route === Left(route)) - assert(msg.finalPayload.amount === finalAmount / 2) - assert(msg.finalPayload.expiry === req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.finalPayload.paymentSecret === pr.paymentSecret.get) - assert(msg.finalPayload.totalAmount === finalAmount) + assert(msg.replyTo == initiator) + assert(msg.route == Left(route)) + assert(msg.finalPayload.amount == finalAmount / 2) + assert(msg.finalPayload.expiry == req.finalExpiry(nodeParams.currentBlockHeight)) + assert(msg.finalPayload.paymentSecret == invoice.paymentSecret) + assert(msg.finalPayload.totalAmount == finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, req))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) - val pf = PaymentFailed(payment.paymentId, pr.paymentHash, Nil) + val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(NoPendingPayment(Right(pr.paymentHash))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(NoPendingPayment(Right(invoice.paymentHash))) } test("forward trampoline payment") { f => import f._ val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), extraHops = ignoredRoutingHints) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = featuresWithTrampoline, extraHops = ignoredRoutingHints) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsgType[SendPaymentConfig] sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId === b) - assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) - assert(msg.totalAmount === finalAmount + trampolineFees) + assert(msg.paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node + assert(msg.targetNodeId == b) + assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) + assert(msg.totalAmount == finalAmount + trampolineFees) assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) - assert(msg.maxAttempts === nodeParams.maxPaymentAttempts) + assert(msg.maxAttempts == nodeParams.maxPaymentAttempts) // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === None) // we're not leaking the invoice secret to the trampoline node - assert(trampolinePayload.invoiceRoutingInfo === None) - assert(trampolinePayload.invoiceFeatures === None) + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingCltv.toLong == currentBlockCount + 9 + 1) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == None) // we're not leaking the invoice secret to the trampoline node + assert(trampolinePayload.invoiceRoutingInfo == None) + assert(trampolinePayload.invoiceFeatures == None) // Verify that the recipient can correctly peel the trampoline onion. - val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(pr.paymentHash), decrypted.nextPacket) + val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(invoice.paymentHash), decrypted.nextPacket) assert(decrypted1.isLastPacket) val finalPayload = PaymentOnionCodecs.finalPerHopPayloadCodec.decode(decrypted1.payload.bits).require.value - assert(finalPayload.amount === finalAmount) - assert(finalPayload.totalAmount === finalAmount) - assert(finalPayload.expiry.toLong === currentBlockCount + 9 + 1) - assert(finalPayload.paymentSecret === pr.paymentSecret.get) + assert(finalPayload.amount == finalAmount) + assert(finalPayload.totalAmount == finalAmount) + assert(finalPayload.expiry.toLong == currentBlockCount + 9 + 1) + assert(finalPayload.paymentSecret == invoice.paymentSecret) } test("forward trampoline to legacy payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some eclair-mobile invoice"), CltvExpiryDelta(9)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some eclair-mobile invoice"), CltvExpiryDelta(9)) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId === b) - assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) - assert(msg.totalAmount === finalAmount + trampolineFees) + assert(msg.paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node + assert(msg.targetNodeId == b) + assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) + assert(msg.totalAmount == finalAmount + trampolineFees) assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === pr.paymentSecret) - assert(trampolinePayload.invoiceFeatures === Some(hex"4100")) // var_onion_optin, payment_secret + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingCltv.toLong == currentBlockCount + 9 + 1) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == invoice.paymentSecret) + assert(trampolinePayload.invoiceFeatures == Some(hex"4100")) // var_onion_optin, payment_secret } test("reject trampoline to legacy payment for 0-value invoice") { f => import f._ // This is disabled because it would let the trampoline node steal the whole payment (if malicious). - val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) - val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = PaymentRequestFeatures(featuresWithMpp)) + val routingHints = List(List(Bolt11Invoice.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = featuresWithMpp) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) - assert(fail.failures === LocalFailure(finalAmount, Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) + assert(fail.id == id) + assert(fail.failures == LocalFailure(finalAmount, Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) multiPartPayFsm.expectNoMessage(50 millis) payFsm.expectNoMessage(50 millis) @@ -340,22 +338,22 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("reject trampoline payment with onion too big") { f => import f._ val paymentMetadata = ByteVector.fromValidHex("01" * 400) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), paymentMetadata = Some(paymentMetadata)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = featuresWithTrampoline, paymentMetadata = Some(paymentMetadata)) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) - assert(fail.failures.length === 1) - assert(fail.failures.head.asInstanceOf[LocalFailure].t.getMessage === "requirement failed: packet per-hop payloads cannot exceed 400 bytes") + assert(fail.id == id) + assert(fail.failures.length == 1) + assert(fail.failures.head.asInstanceOf[LocalFailure].t.getMessage == "requirement failed: packet per-hop payloads cannot exceed 400 bytes") } test("retry trampoline payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -363,19 +361,19 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!cfg.publishEvent) val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) sender.send(initiator, GetPayment(Left(id))) sender.expectMsgType[PaymentIsPending] // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Simulate success which should publish the event and respond to the original sender. - val success = PaymentSent(cfg.parentId, pr.paymentHash, randomBytes32(), finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32(), None))) + val success = PaymentSent(cfg.parentId, invoice.paymentHash, randomBytes32(), finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32(), None))) multiPartPayFsm.send(initiator, success) sender.expectMsg(success) eventListener.expectMsg(success) @@ -388,9 +386,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment and fail") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -398,16 +396,16 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!cfg.publishEvent) val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Simulate a failure that exhausts payment attempts. - val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) multiPartPayFsm.send(initiator, failed) sender.expectMsg(failed) eventListener.expectMsg(failed) @@ -417,26 +415,26 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment and fail (route not found)") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) // Trampoline node couldn't find a route for the given fee. - val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) multiPartPayFsm.send(initiator, failed) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Trampoline node couldn't find a route even with the increased fee. multiPartPayFsm.send(initiator, failed) val failure = sender.expectMsgType[PaymentFailed] - assert(failure.failures === Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) + assert(failure.failures == Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) eventListener.expectMsg(failure) sender.expectNoMessage(100 millis) eventListener.expectNoMessage(100 millis) @@ -444,31 +442,31 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward trampoline payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) val trampolineFees = 100 msat val route = PredefinedNodeRoute(Seq(a, b)) - val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) + val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] assert(payment.trampolineSecret.nonEmpty) - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] - assert(msg.route === Left(route)) - assert(msg.finalPayload.amount === finalAmount + trampolineFees) - assert(msg.finalPayload.paymentSecret === payment.trampolineSecret.get) - assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees) + assert(msg.route == Left(route)) + assert(msg.finalPayload.amount == finalAmount + trampolineFees) + assert(msg.finalPayload.paymentSecret == payment.trampolineSecret) + assert(msg.finalPayload.totalAmount == finalAmount + trampolineFees) assert(msg.finalPayload.isInstanceOf[PaymentOnion.FinalTlvPayload]) val trampolineOnion = msg.finalPayload.asInstanceOf[PaymentOnion.FinalTlvPayload].records.get[OnionPaymentPayloadTlv.TrampolineOnion] assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion.get.packet) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion.get.packet) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === pr.paymentSecret) + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == invoice.paymentSecret) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 824ae6da70..5a8bdc3654 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -18,26 +18,28 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} +import akka.actor.typed.scaladsl.adapter._ import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} import fr.acinq.eclair.channel.Register.{ForwardShortId, ForwardShortIdFailure} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle import fr.acinq.eclair.payment.send.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.makeChannelUpdate -import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.router.BaseRouterSpec.{channelAnnouncement, channelHopFromUpdate} import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ @@ -61,7 +63,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage) val defaultOrigin = Origin.LocalCold(UUID.randomUUID()) val defaultExternalId = UUID.randomUUID().toString - val defaultInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) + val defaultInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) val defaultRouteParams = TestConstants.Alice.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams def defaultRouteRequest(source: PublicKey, target: PublicKey, cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext), routeParams = defaultRouteParams) @@ -96,14 +98,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { result) } - test("send to route") { _ => + test("send to route") { () => val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) + val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -111,15 +113,17 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) - assert(ps.parts.head.route === Some(route.hops)) + assert(ps.id == parentId) + assert(ps.parts.head.route == Some(route.hops)) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route === route) } test("send to route (node_id only)") { routerFixture => @@ -129,7 +133,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = PredefinedNodeRoute(Seq(a, b, c, d)) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext))) @@ -139,21 +143,23 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) } test("send to route (nodes not found in the graph)") { routerFixture => val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -162,13 +168,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(failureMessage == "Not all the nodes in the supplied route are connected with public channels") metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("send to route (channels not found in the graph)") { routerFixture => val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -177,6 +185,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(failureMessage == "The sequence of channels provided cannot be used to build a route to the target node") metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("send to route (routing hints)") { routerFixture => @@ -186,11 +196,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val recipient = randomKey().publicKey val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) - val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144)))) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), routingHint) + val extraEdges = Seq(BasicEdge(c, recipient, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))) + val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), extraEdges) sender.send(paymentFSM, request) - routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext))) + routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, extraEdges, paymentContext = Some(cfg.paymentContext))) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) @@ -200,10 +210,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) } test("payment failed (route not found)") { routerFixture => @@ -211,14 +223,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] - awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.forward(routerFixture.router, routeRequest) - assert(sender.expectMsgType[PaymentFailed].failures === LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -227,6 +239,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 4260000.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (route too expensive)") { routerFixture => @@ -242,7 +256,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = routeParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = routeParams) sender.send(paymentFSM, request) val routeRequest = routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -257,6 +271,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 100.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (cannot build onion)") { routerFixture => @@ -265,16 +281,18 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ val paymentMetadataTooBig = ByteVector.fromValidHex("01" * 1300) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, Some(paymentMetadataTooBig)), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, Some(paymentMetadataTooBig)), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] routerForwarder.forward(routerFixture.router, routeRequest) val pf = sender.expectMsgType[PaymentFailed] - assert(pf.failures.length === 1) + assert(pf.failures.length == 1) assert(pf.failures.head.isInstanceOf[LocalFailure]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (unparsable failure)") { routerFixture => @@ -282,10 +300,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.forward(routerFixture.router) @@ -293,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd1, Nil, _, ignore1, route) = paymentFSM.stateData assert(ignore1.nodes.isEmpty) - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32())))) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes @@ -303,13 +321,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, RouteResponse(route :: Nil)) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, _, ignore2, _) = paymentFSM.stateData - assert(ignore2.nodes === Set(c)) + assert(ignore2.nodes == Set(c)) // and reply a 2nd time with an unparsable failure - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -318,6 +336,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 4260000.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (local error)") { routerFixture => @@ -325,20 +345,20 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, RES_ADD_FAILED(cmd1, ChannelUnavailable(ByteVector32.Zeroes), None)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } test("payment failed (register error)") { routerFixture => @@ -346,9 +366,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -357,8 +377,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { register.send(paymentFSM, ForwardShortIdFailure(fwd)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { routerFixture => @@ -366,9 +386,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -376,12 +396,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32(), FailureMessageCodecs.BADONION)))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } test("payment failed (first htlc failed on-chain)") { routerFixture => @@ -389,9 +409,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -399,7 +419,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFail(HtlcsTimedoutDownstream(randomBytes32(), Set.empty)))) // this error is fatal @@ -412,9 +432,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -422,20 +442,20 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val update_bc_disabled = update_bc.copy(channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = false)) sender.send(paymentFSM, addCompleted(HtlcResult.DisconnectedBeforeSigned(update_bc_disabled))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } test("payment failed (TemporaryChannelFailure)") { routerFixture => val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData @@ -444,11 +464,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val failure = TemporaryChannelFailure(update_bc) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations + assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c))) routerForwarder.forward(routerFixture.router) // payment lifecycle forwards the embedded channelUpdate to the router @@ -457,7 +478,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) } test("payment failed (Update)") { routerFixture => @@ -465,36 +486,36 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, sharedSecrets2, _, route2) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) // we change the cltv expiry one more time - val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2))))) @@ -509,26 +530,29 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (Update in last attempt)") { routerFixture => val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue) val failure = TemporaryChannelFailure(update_bc) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // we should temporarily exclude that channel + assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c))) routerForwarder.expectMsg(update_bc) @@ -542,42 +566,42 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // we build an assisted route for channel bc and cd - val assistedRoutes = Seq(Seq( - ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), - ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) - )) + val extraEdges = Seq( + BasicEdge(b, c, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), + BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) + ) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING - val assistedRoutes1 = Seq(Seq( - ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), - ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) - )) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes1)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING + val extraEdges1 = Seq( + extraEdges(0).asInstanceOf[BasicEdge].update(channelUpdate_bc_modified), + extraEdges(1) + ) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges1)) routerForwarder.forward(routerFixture.router) // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) assert(cmd2.cltvExpiry > cmd1.cltvExpiry) } @@ -587,23 +611,24 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // we build an assisted route for channel cd - val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) + val extraEdges = Seq(BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 1, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we disable the channel - val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false) + val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false) val failure = ChannelDisabled(channelUpdate_cd_disabled.messageFlags, channelUpdate_cd_disabled.channelFlags, channelUpdate_cd_disabled) val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1)._1, failure), sharedSecrets1.head._1) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) + assert(routerForwarder.expectMsgType[RouteCouldRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) routerForwarder.expectMsg(channelUpdate_cd_disabled) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_cd.shortChannelId, c, d))) } @@ -613,9 +638,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -623,16 +648,16 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_bc, b, c))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_bc, b, c))))) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -651,24 +676,24 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = eventListener.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) assert(ps.feesPaid > 0.msat) - assert(ps.recipientAmount === defaultAmountMsat) - assert(ps.paymentHash === defaultPaymentHash) - assert(ps.paymentPreimage === defaultPaymentPreimage) - assert(ps.parts.head.id === id) + assert(ps.recipientAmount == defaultAmountMsat) + assert(ps.paymentHash == defaultPaymentHash) + assert(ps.paymentPreimage == defaultPaymentPreimage) + assert(ps.parts.head.id == id) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -677,6 +702,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 730.msat) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc, update_cd).map(_.shortChannelId)) } test("payment succeeded to a channel with fees=0") { routerFixture => @@ -688,16 +715,16 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // | | and b -> h has fees = 0 // +---(5)--> g ---(6)--> h // and e --(4)--> f (we are a) - val channelId_bh = ShortChannelId(BlockHeight(420000), 100, 0) + val channelId_bh = RealShortChannelId(BlockHeight(420000), 100, 0) val chan_bh = channelAnnouncement(channelId_bh, priv_b, priv_h, priv_funding_b, priv_funding_h) val channelUpdate_bh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, h, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) val channelUpdate_hb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, b, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) - assert(Router.getDesc(channelUpdate_bh, chan_bh) === ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) + assert(ChannelDesc(channelUpdate_bh, chan_bh) == ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) val peerConnection = TestProbe() router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_bh) router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_bh) router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_hb) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_bh) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_bh) watcher.send(router, ValidateResult(chan_bh, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_h)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) watcher.expectMsgType[WatchExternalChannelSpent] @@ -705,7 +732,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ // we send a payment to H - val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] @@ -717,12 +744,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, request.finalPayload.amount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent] - assert(finalAmount === defaultAmountMsat) + assert(finalAmount == defaultAmountMsat) // NB: A -> B doesn't pay fees because it's our direct neighbor // NB: B -> H doesn't asks for fees at all - assert(fee === 0.msat) - assert(paymentOK.recipientAmount === request.finalPayload.amount) + assert(fee == 0.msat) + assert(paymentOK.recipientAmount == request.finalPayload.amount) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -730,48 +757,50 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 0.msat) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, channelUpdate_bh).map(_.shortChannelId)) } - test("filter errors properly") { _ => + test("filter errors properly") { () => val failures = Seq( LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)), + RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)), LocalFailure(defaultAmountMsat, Nil, RouteNotFound) ) val filtered = PaymentFailure.transformForUser(failures) val expected = Seq( LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) + RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) ) - assert(filtered === expected) + assert(filtered == expected) } - test("ignore failed nodes/channels") { _ => - val route_abcd = ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil + test("ignore failed nodes/channels") { () => + val route_abcd = channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil val testCases = Seq( // local failures -> ignore first channel if there is one (LocalFailure(defaultAmountMsat, Nil, RouteNotFound), Set.empty, Set.empty), (LocalFailure(defaultAmountMsat, NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty), - (LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(channelId_ab, a, b))), + (LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))), // remote failure from final recipient -> all intermediate nodes behaved correctly (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty), // remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure)), Set(b), Set.empty), (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), Set(c), Set.empty), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(channelId_bc, b, c))), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(channelId_cd, c, d))), + (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(scid_bc, b, c))), + (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(scid_cd, c, d))), (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), // unreadable remote failures -> blacklist all nodes except our direct peer and the final recipient - (UnreadableRemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: ChannelHop(d, e, null) :: Nil), Set(c, d), Set.empty) + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil), Set.empty, Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: ChannelHop(ShortChannelId(5656986L), d, e, null) :: Nil), Set(c, d), Set.empty) ) for ((failure, expectedNodes, expectedChannels) <- testCases) { val ignore = PaymentFailure.updateIgnored(failure, Ignore.empty) - assert(ignore.nodes === expectedNodes, failure) - assert(ignore.channels === expectedChannels, failure) + assert(ignore.nodes == expectedNodes, failure) + assert(ignore.channels == expectedChannels, failure) } val failures = Seq( @@ -780,8 +809,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")) ) val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty) - assert(ignore.nodes === Set(c)) - assert(ignore.channels === Set(ChannelDesc(channelId_ab, a, b), ChannelDesc(channelId_bc, b, c))) + assert(ignore.nodes == Set(c)) + assert(ignore.channels == Set(ChannelDesc(scid_ab, a, b), ChannelDesc(scid_bc, b, c))) } test("disable database and events") { routerFixture => @@ -789,28 +818,30 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(nodeParams.db.payments.getOutgoingPayment(id) === None) + assert(nodeParams.db.payments.getOutgoingPayment(id) == None) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) sender.expectMsgType[PaymentSent] - assert(nodeParams.db.payments.getOutgoingPayment(id) === None) + assert(nodeParams.db.payments.getOutgoingPayment(id) == None) eventListener.expectNoMessage(100 millis) + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nextNodeId) == Seq(b, c, d)) } - test("send to route (no retry on error") { _ => + test("send to route (no retry on error") { () => val payFixture = createPaymentLifecycle() import payFixture._ import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) + val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -819,10 +850,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _) = paymentFSM.stateData awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 9fd9c8eec1..c528512e4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -17,22 +17,23 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPaymentPacket._ -import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -58,15 +59,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000 val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000 val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat) - assert(ref === fee) + assert(ref == fee) } def testBuildOnion(): Unit = { val finalPayload = FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, 0 msat))) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, finalPayload) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) - assert(onion.packet.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) + assert(onion.packet.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion testPeelOnion(onion.packet) @@ -75,41 +76,41 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { def testPeelOnion(packet_b: OnionRoutingPacket): Unit = { val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, packet_b) val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(packet_c.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_b.amountToForward === amount_bc) - assert(payload_b.outgoingCltv === expiry_bc) - assert(payload_b.outgoingChannelId === channelUpdate_bc.shortChannelId) - assert(relay_b.relayFeeMsat === fee_b) - assert(relay_b.expiryDelta === channelUpdate_bc.cltvExpiryDelta) + assert(add_b2 == add_b) + assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_b.amountToForward == amount_bc) + assert(payload_b.outgoingCltv == expiry_bc) + assert(payload_b.outgoingChannelId == channelUpdate_bc.shortChannelId) + assert(relay_b.relayFeeMsat == fee_b) + assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c) val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(add_c2 === add_c) - assert(packet_d.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_c.amountToForward === amount_cd) - assert(payload_c.outgoingCltv === expiry_cd) - assert(payload_c.outgoingChannelId === channelUpdate_cd.shortChannelId) - assert(relay_c.relayFeeMsat === fee_c) - assert(relay_c.expiryDelta === channelUpdate_cd.cltvExpiryDelta) + assert(add_c2 == add_c) + assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_c.amountToForward == amount_cd) + assert(payload_c.outgoingCltv == expiry_cd) + assert(payload_c.outgoingChannelId == channelUpdate_cd.shortChannelId) + assert(relay_c.relayFeeMsat == fee_c) + assert(relay_c.expiryDelta == channelUpdate_cd.cltvExpiryDelta) val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d) val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey) - assert(add_d2 === add_d) - assert(packet_e.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_d.amountToForward === amount_de) - assert(payload_d.outgoingCltv === expiry_de) - assert(payload_d.outgoingChannelId === channelUpdate_de.shortChannelId) - assert(relay_d.relayFeeMsat === fee_d) - assert(relay_d.expiryDelta === channelUpdate_de.cltvExpiryDelta) + assert(add_d2 == add_d) + assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_d.amountToForward == amount_de) + assert(payload_d.outgoingCltv == expiry_de) + assert(payload_d.outgoingChannelId == channelUpdate_de.shortChannelId) + assert(relay_d.relayFeeMsat == fee_d) + assert(relay_d.expiryDelta == channelUpdate_de.cltvExpiryDelta) val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e) val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey) - assert(add_e2 === add_e) - assert(payload_e.amount === finalAmount) - assert(payload_e.totalAmount === finalAmount) - assert(payload_e.expiry === finalExpiry) - assert(payload_e.paymentSecret === paymentSecret) + assert(add_e2 == add_e) + assert(payload_e.amount == finalAmount) + assert(payload_e.totalAmount == finalAmount) + assert(payload_e.expiry == finalExpiry) + assert(payload_e.paymentSecret contains paymentSecret) } test("build onion with final payload") { @@ -117,32 +118,32 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command including the onion") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) assert(add.amount > finalAmount) - assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) - assert(add.paymentHash === paymentHash) - assert(add.onion.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + assert(add.cltvExpiry == finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) + assert(add.paymentHash == paymentHash) + assert(add.onion.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion testPeelOnion(add.onion) } test("build a command with no hops") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata))) - assert(add.amount === finalAmount) - assert(add.cltvExpiry === finalExpiry) - assert(add.paymentHash === paymentHash) - assert(add.onion.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), Some(paymentMetadata))) + assert(add.amount == finalAmount) + assert(add.cltvExpiry == finalExpiry) + assert(add.paymentHash == paymentHash) + assert(add.onion.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount, paymentHash, finalExpiry, add.onion) val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(payload_b.amount === finalAmount) - assert(payload_b.totalAmount === finalAmount) - assert(payload_b.expiry === finalExpiry) - assert(payload_b.paymentSecret === paymentSecret) - assert(payload_b.paymentMetadata === Some(paymentMetadata)) + assert(add_b2 == add_b) + assert(payload_b.amount == finalAmount) + assert(payload_b.totalAmount == finalAmount) + assert(payload_b.expiry == finalExpiry) + assert(payload_b.paymentSecret contains paymentSecret) + assert(payload_b.paymentMetadata contains paymentMetadata) } test("build a trampoline payment") { @@ -151,59 +152,59 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ / \ // a -> b -> c d e - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"))) - assert(amount_ac === amount_bc) - assert(expiry_ac === expiry_bc) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, Some(paymentSecret), Some(hex"010203"))) + assert(amount_ac == amount_bc) + assert(expiry_ac == expiry_bc) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(payload_b === ChannelRelayTlvPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) + assert(add_b2 == add_b) + assert(payload_b == ChannelRelayTlvPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c) val Right(NodeRelayPacket(add_c2, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(add_c2 === add_c) - assert(outer_c.amount === amount_bc) - assert(outer_c.totalAmount === amount_bc) - assert(outer_c.expiry === expiry_bc) - assert(inner_c.amountToForward === amount_cd) - assert(inner_c.outgoingCltv === expiry_cd) - assert(inner_c.outgoingNodeId === d) - assert(inner_c.invoiceRoutingInfo === None) - assert(inner_c.invoiceFeatures === None) - assert(inner_c.paymentSecret === None) - assert(inner_c.paymentMetadata === None) + assert(add_c2 == add_c) + assert(outer_c.amount == amount_bc) + assert(outer_c.totalAmount == amount_bc) + assert(outer_c.expiry == expiry_bc) + assert(inner_c.amountToForward == amount_cd) + assert(inner_c.outgoingCltv == expiry_cd) + assert(inner_c.outgoingNodeId == d) + assert(inner_c.invoiceRoutingInfo == None) + assert(inner_c.invoiceFeatures == None) + assert(inner_c.paymentSecret == None) + assert(inner_c.paymentMetadata == None) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) - assert(amount_d === amount_cd) - assert(expiry_d === expiry_cd) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + assert(amount_d == amount_cd) + assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) val Right(NodeRelayPacket(add_d2, outer_d, inner_d, packet_e)) = decrypt(add_d, priv_d.privateKey) - assert(add_d2 === add_d) - assert(outer_d.amount === amount_cd) - assert(outer_d.totalAmount === amount_cd) - assert(outer_d.expiry === expiry_cd) - assert(inner_d.amountToForward === amount_de) - assert(inner_d.outgoingCltv === expiry_de) - assert(inner_d.outgoingNodeId === e) - assert(inner_d.invoiceRoutingInfo === None) - assert(inner_d.invoiceFeatures === None) - assert(inner_d.paymentSecret === None) - assert(inner_d.paymentMetadata === None) + assert(add_d2 == add_d) + assert(outer_d.amount == amount_cd) + assert(outer_d.totalAmount == amount_cd) + assert(outer_d.expiry == expiry_cd) + assert(inner_d.amountToForward == amount_de) + assert(inner_d.outgoingCltv == expiry_de) + assert(inner_d.outgoingNodeId == e) + assert(inner_d.invoiceRoutingInfo == None) + assert(inner_d.invoiceFeatures == None) + assert(inner_d.paymentSecret == None) + assert(inner_d.paymentMetadata == None) // d forwards the trampoline payment to e. - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) - assert(amount_e === amount_de) - assert(expiry_e === expiry_de) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) + assert(amount_e == amount_de) + assert(expiry_e == expiry_de) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet) val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey) - assert(add_e2 === add_e) - assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) + assert(add_e2 == add_e) + assert(payload_e == FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) } test("build a trampoline payment with non-trampoline recipient") { @@ -212,68 +213,68 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ // a -> b -> c d -> e - val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) - val invoiceFeatures = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)) - val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) - assert(amount_ac === amount_bc) - assert(expiry_ac === expiry_bc) + val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) + val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret, None)) + assert(amount_ac == amount_bc) + assert(expiry_ac == expiry_bc) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c) val Right(NodeRelayPacket(_, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(outer_c.amount === amount_bc) - assert(outer_c.totalAmount === amount_bc) - assert(outer_c.expiry === expiry_bc) + assert(outer_c.amount == amount_bc) + assert(outer_c.totalAmount == amount_bc) + assert(outer_c.expiry == expiry_bc) assert(outer_c.paymentSecret !== invoice.paymentSecret) - assert(inner_c.amountToForward === amount_cd) - assert(inner_c.outgoingCltv === expiry_cd) - assert(inner_c.outgoingNodeId === d) - assert(inner_c.invoiceRoutingInfo === None) - assert(inner_c.invoiceFeatures === None) - assert(inner_c.paymentSecret === None) + assert(inner_c.amountToForward == amount_cd) + assert(inner_c.outgoingCltv == expiry_cd) + assert(inner_c.outgoingNodeId == d) + assert(inner_c.invoiceRoutingInfo == None) + assert(inner_c.invoiceFeatures == None) + assert(inner_c.paymentSecret == None) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) - assert(amount_d === amount_cd) - assert(expiry_d === expiry_cd) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + assert(amount_d == amount_cd) + assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) val Right(NodeRelayPacket(_, outer_d, inner_d, _)) = decrypt(add_d, priv_d.privateKey) - assert(outer_d.amount === amount_cd) - assert(outer_d.totalAmount === amount_cd) - assert(outer_d.expiry === expiry_cd) + assert(outer_d.amount == amount_cd) + assert(outer_d.totalAmount == amount_cd) + assert(outer_d.expiry == expiry_cd) assert(outer_d.paymentSecret !== invoice.paymentSecret) - assert(inner_d.amountToForward === finalAmount) - assert(inner_d.outgoingCltv === expiry_de) - assert(inner_d.outgoingNodeId === e) - assert(inner_d.totalAmount === finalAmount) - assert(inner_d.paymentSecret === invoice.paymentSecret) - assert(inner_d.paymentMetadata === Some(hex"010203")) - assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp - assert(inner_d.invoiceRoutingInfo === Some(routingHints)) + assert(inner_d.amountToForward == finalAmount) + assert(inner_d.outgoingCltv == expiry_de) + assert(inner_d.outgoingNodeId == e) + assert(inner_d.totalAmount == finalAmount) + assert(inner_d.paymentSecret == invoice.paymentSecret) + assert(inner_d.paymentMetadata == Some(hex"010203")) + assert(inner_d.invoiceFeatures == Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp + assert(inner_d.invoiceRoutingInfo == Some(routingHints)) } test("fail to build a trampoline payment when too much invoice data is provided") { - val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) - val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) - assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata)).isFailure) + val routingHintOverflow = List(List.fill(7)(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) + assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret, invoice.paymentMetadata)).isFailure) } test("fail to decrypt when the onion is invalid") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse)) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when the trampoline onion is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey) @@ -283,72 +284,72 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt when payment hash doesn't match associated data") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt at the final node when amount has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) - assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat)) + assert(failure == FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) - assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) + assert(failure == FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey) // d forwards an invalid amount to e (the outer total amount doesn't match the inner amount). val invalidTotalAmount = amount_de + 100.msat - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) - assert(failure === FinalIncorrectHtlcAmount(invalidTotalAmount)) + assert(failure == FinalIncorrectHtlcAmount(invalidTotalAmount)) } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey) // d forwards an invalid expiry to e (the outer expiry doesn't match the inner expiry). val invalidExpiry = expiry_de - CltvExpiryDelta(12) - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) - assert(failure === FinalIncorrectCltvExpiry(invalidExpiry)) + assert(failure == FinalIncorrectCltvExpiry(invalidExpiry)) } test("fail to decrypt at intermediate trampoline node when amount is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c), priv_c.privateKey) - assert(failure === FinalIncorrectHtlcAmount(amount_bc - 100.msat)) + assert(failure == FinalIncorrectHtlcAmount(amount_bc - 100.msat)) } test("fail to decrypt at intermediate trampoline node when expiry is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c), priv_c.privateKey) - assert(failure === FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12))) + assert(failure == FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12))) } } @@ -367,11 +368,12 @@ object PaymentPacketSpec { Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, Some(associatedData)).get.packet } - def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { - val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null) - val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat, channelFeatures: ChannelFeatures = ChannelFeatures()): Commitments = { + val params = LocalParams(null, null, null, null, None, null, null, 0, isInitiator = true, null, None, null) + val remoteParams = RemoteParams(randomKey().publicKey, null, null, None, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) - new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { + val channelFlags = ChannelFlags.Private + new Commitments(channelId, ChannelConfig.standard, channelFeatures, params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } @@ -391,10 +393,10 @@ object PaymentPacketSpec { // simple route a -> b -> c -> d -> e val hops = - ChannelHop(a, b, channelUpdate_ab) :: - ChannelHop(b, c, channelUpdate_bc) :: - ChannelHop(c, d, channelUpdate_cd) :: - ChannelHop(d, e, channelUpdate_de) :: Nil + channelHopFromUpdate(a, b, channelUpdate_ab) :: + channelHopFromUpdate(b, c, channelUpdate_bc) :: + channelHopFromUpdate(c, d, channelUpdate_cd) :: + channelHopFromUpdate(d, e, channelUpdate_de) :: Nil val finalAmount = 42000000 msat val currentBlockCount = 400000 @@ -406,15 +408,15 @@ object PaymentPacketSpec { val expiry_de = finalExpiry val amount_de = finalAmount - val fee_d = nodeFee(channelUpdate_de, amount_de) + val fee_d = nodeFee(channelUpdate_de.relayFees, amount_de) val expiry_cd = expiry_de + channelUpdate_de.cltvExpiryDelta val amount_cd = amount_de + fee_d - val fee_c = nodeFee(channelUpdate_cd, amount_cd) + val fee_c = nodeFee(channelUpdate_cd.relayFees, amount_cd) val expiry_bc = expiry_cd + channelUpdate_cd.cltvExpiryDelta val amount_bc = amount_cd + fee_c - val fee_b = nodeFee(channelUpdate_bc, amount_bc) + val fee_b = nodeFee(channelUpdate_bc.relayFees, amount_bc) val expiry_ab = expiry_bc + channelUpdate_bc.cltvExpiryDelta val amount_ab = amount_bc + fee_b @@ -430,7 +432,7 @@ object PaymentPacketSpec { NodeHop(d, e, channelUpdate_de.cltvExpiryDelta, fee_d) :: Nil val trampolineChannelHops = - ChannelHop(a, b, channelUpdate_ab) :: - ChannelHop(b, c, channelUpdate_bc) :: Nil + channelHopFromUpdate(a, b, channelUpdate_ab) :: + channelHopFromUpdate(b, c, channelUpdate_bc) :: Nil } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala deleted file mode 100644 index 7da8dcbe96..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.payment - -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} -import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} -import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} -import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} -import org.scalatest.funsuite.AnyFunSuite -import scodec.DecodeResult -import scodec.bits._ -import scodec.codecs.bits - -import scala.util.Success - -/** - * Created by fabrice on 15/05/17. - */ - -class PaymentRequestSpec extends AnyFunSuite { - - val priv = PrivateKey(hex"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734") - val pub = priv.publicKey - val nodeId = pub - assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - - test("check minimal unit is used") { - assert('p' === Amount.unit(1 msat)) - assert('p' === Amount.unit(99 msat)) - assert('n' === Amount.unit(100 msat)) - assert('p' === Amount.unit(101 msat)) - assert('n' === Amount.unit((1 sat).toMilliSatoshi)) - assert('u' === Amount.unit((100 sat).toMilliSatoshi)) - assert('n' === Amount.unit((101 sat).toMilliSatoshi)) - assert('u' === Amount.unit((1155400 sat).toMilliSatoshi)) - assert('m' === Amount.unit((1 millibtc).toMilliSatoshi)) - assert('m' === Amount.unit((10 millibtc).toMilliSatoshi)) - assert('m' === Amount.unit((1 btc).toMilliSatoshi)) - } - - test("decode empty amount") { - assert(Amount.decode("") === Success(None)) - assert(Amount.decode("0") === Success(None)) - assert(Amount.decode("0p") === Success(None)) - assert(Amount.decode("0n") === Success(None)) - assert(Amount.decode("0u") === Success(None)) - assert(Amount.decode("0m") === Success(None)) - } - - test("check that we can still decode non-minimal amount encoding") { - assert(Amount.decode("1000u") === Success(Some(100000000 msat))) - assert(Amount.decode("1000000n") === Success(Some(100000000 msat))) - assert(Amount.decode("1000000000p") === Success(Some(100000000 msat))) - } - - test("data string -> bitvector") { - assert(string2Bits("p") === bin"00001") - assert(string2Bits("pz") === bin"0000100010") - } - - test("minimal length long, left-padded to be multiple of 5") { - assert(long2bits(0) == bin"") - assert(long2bits(1) == bin"00001") - assert(long2bits(42) == bin"0000101010") - assert(long2bits(255) == bin"0011111111") - assert(long2bits(256) == bin"0100000000") - assert(long2bits(3600) == bin"000111000010000") - } - - test("verify that padding is zero") { - val codec = PaymentRequest.Codecs.alignedBytesCodec(bits) - assert(codec.decode(bin"1010101000").require == DecodeResult(bin"10101010", BitVector.empty)) - assert(codec.decode(bin"1010101001").isFailure) // non-zero padding - } - - test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") { - val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount.isEmpty) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("Please consider supporting this project")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 4) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("Please send $3 for a cup of coffee to the same peer, within 1 minute") { - val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(250000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("1 cup coffee")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") { - val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(250000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("ナンセンス 1杯")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("Now send $24 for an entire list of things (hashed)") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 4) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") { - val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lntb") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) - assert(pr.routingInfo === List(List( - ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("66051x263430x1800"), 1 msat, 20, CltvExpiryDelta(3)), - ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("197637x395016x2314"), 2 msat, 30, CltvExpiryDelta(4)) - ))) - assert(pr.tags.size == 6) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(!pr.features.allowMultiPart) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") { - val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(!pr.features.allowMultiPart) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(12))) - assert(pr.tags.size == 6) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { - val refs = Seq( - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa", - // All upper-case - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".toUpperCase, - // With ignored fields - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x" - ) - - for (ref <- refs) { - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(2500000000L msat)) - assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === TimestampSecond(1496314658L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("coffee beans")) - assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") - assert(!pr.features.allowMultiPart) - assert(pr.features.requirePaymentSecret) - assert(!pr.features.allowTrampoline) - assert(pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(PaymentRequest.write(pr.sign(priv)) === ref.toLowerCase) - } - } - - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { - val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(2500000000L msat)) - assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === TimestampSecond(1496314658L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("coffee beans")) - assert(pr.fallbackAddress().isEmpty) - assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") - assert(!pr.features.allowMultiPart) - assert(pr.features.requirePaymentSecret) - assert(!pr.features.allowTrampoline) - assert(!pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(PaymentRequest.write(pr.sign(priv)) === ref) - } - - test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { - val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(967878534 msat)) - assert(pr.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(pr.timestamp === TimestampSecond(1572468703L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) - assert(pr.fallbackAddress().isEmpty) - assert(pr.expiry === Some(604800L)) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10))) - assert(pr.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) - assert(PaymentRequest.write(pr.sign(priv)) === ref) - } - - test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { - val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(1000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.description == Left("payment metadata inside")) - assert(pr.paymentMetadata === Some(hex"01fafaf0")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) - } - - test("reject invalid invoices") { - val refs = Seq( - // Bech32 checksum is invalid. - "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt", - // Malformed bech32 string (no 1). - "pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny", - // Malformed bech32 string (mixed case). - "LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny", - // Signature is not recoverable. - "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2", - // String is too short. - "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh", - // Invalid multiplier. - "lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqrrzc4cvfue4zp3hggxp47ag7xnrlr8vgcmkjxk3j5jqethnumgkpqp23z9jclu3v0a7e0aruz366e9wqdykw6dxhdzcjjhldxq0w6wgqcnu43j", - // Invalid sub-millisatoshi precision. - "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" - ) - for (ref <- refs) { - assertThrows[Exception](PaymentRequest.read(ref)) - } - } - - test("correctly serialize/deserialize variable-length tagged fields") { - val number = 123456 - val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits).as[PaymentRequest.Expiry] - val field = PaymentRequest.Expiry(number) - assert(field.toLong == number) - - val serializedExpiry = codec.encode(field).require - val field1 = codec.decodeValue(serializedExpiry).require - assert(field1 == field) - - // Now with a payment request - val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(18))) - val serialized = PaymentRequest.write(pr) - val pr1 = PaymentRequest.read(serialized) - assert(pr == pr1) - } - - test("ignore unknown tags") { - val pr = PaymentRequest( - prefix = "lntb", - amount = Some(100000 msat), - timestamp = TimestampSecond.now(), - nodeId = nodeId, - tags = List( - PaymentHash(ByteVector32(ByteVector.fill(32)(1))), - Description("description"), - UnknownTag21(BitVector("some data we don't understand".getBytes)) - ), - signature = ByteVector.empty).sign(priv) - - val serialized = PaymentRequest.write(pr) - val pr1 = PaymentRequest.read(serialized) - val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } - } - - test("ignore hash tags with invalid length") { - // Bolt11: A reader: MUST skip over p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively. - def bits(i: Int) = BitVector.fill(i * 5)(high = false) - - val inputs = Map( - "ppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(51)), - "pp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag1(bits(53)), - "hpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(51)), - "hp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag23(bits(53)), - "spnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(51)), - "sp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> InvalidTag16(bits(53)), - "np5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(52)), - "npkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" -> UnknownTag19(bits(54)) - ) - - for ((input, value) <- inputs) { - val data = string2Bits(input) - val decoded = Codecs.taggedFieldCodec.decode(data).require.value - assert(decoded === value) - val encoded = Codecs.taggedFieldCodec.encode(value).require - assert(encoded === data) - } - } - - test("accept uppercase payment request") { - val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j" - assert(PaymentRequest.write(PaymentRequest.read(input.toUpperCase())) == input) - } - - test("Pay 1 BTC without multiplier") { - val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" - val pr = PaymentRequest.read(ref) - assert(pr.amount === Some(100000000000L msat)) - assert(pr.features.bitmask === BitVector.empty) - } - - test("supported payment request features") { - val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> FeatureSupport.Optional).toMap)) - case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule - val featureBits = Map( - PaymentRequestFeatures(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) - PaymentRequestFeatures(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), - PaymentRequestFeatures(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), - PaymentRequestFeatures(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) - ) - - for ((features, res) <- featureBits) { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) - assert(Result(pr.features.allowMultiPart, pr.features.requirePaymentSecret, pr.features.areSupported(nodeParams)) === res) - assert(PaymentRequest.read(PaymentRequest.write(pr)) === pr) - } - } - - test("feature bits to minimally-encoded feature bytes") { - val testCases = Seq( - (bin" 0010000100000101", hex" 2105"), - (bin" 1010000100000101", hex" a105"), - (bin" 11000000000000110", hex"018006"), - (bin" 01000000000000110", hex" 8006"), - (bin" 001000000000000000", hex" 8000"), - (bin" 101000000000000000", hex"028000"), - (bin"0101010000000000110", hex"02a006"), - (bin"1000110000000000110", hex"046006") - ) - - for ((bitmask, featureBytes) <- testCases) { - assert(PaymentRequestFeatures(bitmask).toByteVector === featureBytes) - } - } - - test("payment secret") { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(pr.paymentSecret.isDefined) - assert(pr.features === PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory))) - assert(pr.features.requirePaymentSecret) - - val pr1 = PaymentRequest.read(PaymentRequest.write(pr)) - assert(pr1.paymentSecret === pr.paymentSecret) - - val pr2 = PaymentRequest.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr2.features.requirePaymentSecret) - assert(pr2.paymentSecret === None) - - // An invoice that sets the payment secret feature bit must provide a payment secret. - assertThrows[IllegalArgumentException]( - PaymentRequest.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7") - ) - - // A multi-part invoice must use a payment secret. - assertThrows[IllegalArgumentException]({ - val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Optional)) - PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = features) - }) - } - - test("trampoline") { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(!pr.features.allowTrampoline) - - val features1 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional)) - val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features1) - assert(!pr1.features.allowMultiPart) - assert(pr1.features.allowTrampoline) - - val features2 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional)) - val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features2) - assert(pr2.features.allowMultiPart) - assert(pr2.features.allowTrampoline) - - val pr3 = PaymentRequest.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr3.features.allowTrampoline) - } - - test("nonreg") { - val requests = List( - "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl", - "lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd", - "lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex", - "lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque", - "lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0", - "lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54", - "lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe", - "lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf", - "lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc", - "lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3", - "lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw", - "lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28", - "lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah", - "lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv", - "lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d", - "lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9", - "lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3", - "lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t", - "lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee", - "lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6", - "lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h", - "lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv", - "lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww", - "lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr", - "lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad", - "lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw", - "lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e", - "lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr", - "lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u", - "lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j", - "lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5", - "lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda", - "lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu", - "lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu", - "lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733", - "lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh", - "lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7", - "lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g", - "lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v", - "lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0", - "lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838", - "lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx", - "lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer", - "lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr", - "lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0", - "lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x", - "lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra", - "lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r", - "lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w", - "lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y", - "lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53", - "lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz", - "lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua", - "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p", - "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx", - "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9", - "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" - ) - - for (req <- requests) { - assert(PaymentRequest.write(PaymentRequest.read(req)) == req) - } - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 15beebcb7f..2ccc0ca29f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -20,20 +20,23 @@ import akka.Done import akka.actor.ActorRef import akka.event.LoggingAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.ChannelHop +import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, ParallelTestExecution} import scodec.bits.ByteVector @@ -47,7 +50,7 @@ import scala.util.Success * Created by t-bast on 21/11/2019. */ -class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ParallelTestExecution with ChannelStateTestsHelperMethods { +class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ParallelTestExecution with ChannelStateTestsBase { import PostRestartHtlcCleanerSpec._ @@ -120,19 +123,19 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val fails_ab_1 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_1.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_1.toSet == Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) val fails_ab_2 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_2.toSet === Set(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_2.toSet == Set(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the fails, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val fails_ab_1_bis = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_1_bis.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_1_bis.toSet == Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // let's now assume that channel 1 gets reconnected, and it had the time to fail the htlcs: @@ -150,7 +153,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) - val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.nodeKeyManager.nodeKey.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.nodeKeyManager.nodeKey.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) nodeParams.db.payments.addIncomingPayment(invoice, preimage) nodeParams.db.payments.receiveIncomingPayment(paymentHash, 5000 msat) @@ -188,7 +191,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure), commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) - assert(received1 === expected1) + assert(received1 == expected1) channel.expectNoMessage(100 millis) // channel 2 goes to NORMAL state: @@ -200,13 +203,13 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure), commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) - assert(received2 === expected2) + assert(received2 == expected2) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the updates, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val received3 = expected1.map(_ => channel.expectMsgType[Command]) - assert(received3 === expected1) + assert(received3 == expected1) channel.expectNoMessage(100 millis) // let's now assume that channel 1 gets reconnected, and it had the time to sign the htlcs: @@ -229,20 +232,20 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, testCase.fails(1)) eventListener.expectNoMessage(100 millis) // This is a multi-part payment, the second part is still pending. - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fails(2)) val e1 = eventListener.expectMsgType[PaymentFailed] - assert(e1.id === testCase.parentId) - assert(e1.paymentHash === paymentHash2) + assert(e1.id == testCase.parentId) + assert(e1.paymentHash == paymentHash2) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fails.head) val e2 = eventListener.expectMsgType[PaymentFailed] - assert(e2.id === testCase.childIds.head) - assert(e2.paymentHash === paymentHash1) + assert(e2.id == testCase.childIds.head) + assert(e2.paymentHash == paymentHash1) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) register.expectNoMessage(100 millis) @@ -258,33 +261,33 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, testCase.fulfills(1)) eventListener.expectNoMessage(100 millis) // This is a multi-part payment, the second part is still pending. - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fulfills(2)) val e1 = eventListener.expectMsgType[PaymentSent] - assert(e1.id === testCase.parentId) - assert(e1.paymentPreimage === preimage2) - assert(e1.paymentHash === paymentHash2) - assert(e1.parts.length === 2) - assert(e1.amountWithFees === 2834.msat) - assert(e1.recipientAmount === 2500.msat) + assert(e1.id == testCase.parentId) + assert(e1.paymentPreimage == preimage2) + assert(e1.paymentHash == paymentHash2) + assert(e1.parts.length == 2) + assert(e1.amountWithFees == 2834.msat) + assert(e1.recipientAmount == 2500.msat) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fulfills.head) val e2 = eventListener.expectMsgType[PaymentSent] - assert(e2.id === testCase.childIds.head) - assert(e2.paymentPreimage === preimage1) - assert(e2.paymentHash === paymentHash1) - assert(e2.parts.length === 1) - assert(e2.recipientAmount === 561.msat) + assert(e2.id == testCase.childIds.head) + assert(e2.paymentPreimage == preimage1) + assert(e2.paymentHash == paymentHash1) + assert(e2.parts.length == 1) + assert(e2.recipientAmount == 561.msat) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) register.expectNoMessage(100 millis) } - test("ignore htlcs in closing downstream channels that have already been settled upstream") { f => + test("ignore htlcs in downstream channels that have already been settled upstream") { f => import f._ val testCase = setupTrampolinePayments(nodeParams) @@ -296,8 +299,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val probe = TestProbe() probe.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs) val brokenHtlcs = probe.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs] - assert(brokenHtlcs.notRelayed.map(htlc => (htlc.add.id, htlc.add.channelId)).toSet === testCase.notRelayed) - assert(brokenHtlcs.relayedOut === Map( + assert(brokenHtlcs.notRelayed.map(htlc => (htlc.add.id, htlc.add.channelId)).toSet == testCase.notRelayed) + assert(brokenHtlcs.relayedOut == Map( testCase.origin_1 -> Set(testCase.downstream_1_1).map(htlc => (htlc.channelId, htlc.id)), testCase.origin_2 -> Set(testCase.downstream_2_1, testCase.downstream_2_2, testCase.downstream_2_3).map(htlc => (htlc.channelId, htlc.id)) )) @@ -380,8 +383,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val closingState = localClose(alice, alice2blockchain) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) // All committed htlcs timed out except the last two; one will be fulfilled later and the other will timeout later. - assert(closingState.htlcTxs.size === 4) - assert(getHtlcTimeoutTxs(closingState).length === 4) + assert(closingState.htlcTxs.size == 4) + assert(getHtlcTimeoutTxs(closingState).length == 4) val htlcTxs = getHtlcTimeoutTxs(closingState).sortBy(_.tx.txOut.map(_.amount).sum) htlcTxs.reverse.drop(2).zipWithIndex.foreach { case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx.tx) @@ -412,8 +415,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val origin_2 = Origin.TrampolineRelayedCold(upstream_2.adds.map(u => (u.channelId, u.id)).toList) sender.send(relayer, RES_ADD_SETTLED(origin_2, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) register.expectMsgAllOf( - Register.Forward(replyTo = ActorRef.noSender, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)), - Register.Forward(replyTo = ActorRef.noSender, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true)) + Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)), + Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true)) ) // Payment 3 should not be failed: we are still waiting for on-chain confirmation. @@ -441,6 +444,50 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit assert(brokenHtlcs.notRelayed.isEmpty) } + test("ignore htlcs in downstream revoked close") { f => + import f._ + + val (paymentHash1, paymentHash2) = (randomBytes32(), randomBytes32()) + val htlc_ab = Seq( + buildHtlcIn(1, channelId_ab_1, paymentHash1), + buildHtlcIn(2, channelId_ab_1, paymentHash1), + buildHtlcIn(4, channelId_ab_1, paymentHash2), + ) + val upstreamChannel = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) + val htlc_bc = Seq( + buildHtlcOut(2, channelId_bc_1, paymentHash1), + buildHtlcOut(3, channelId_bc_1, paymentHash1), + buildHtlcOut(4, channelId_bc_1, paymentHash1), + buildHtlcOut(5, channelId_bc_1, paymentHash2), + ) + val origins: Map[Long, Origin] = Map( + 2L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 3L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 4L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 5L -> Origin.ChannelRelayedCold(channelId_ab_1, 4, 550 msat, 500 msat), + ) + val downstreamChannel = { + val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins) + // NB: this isn't actually a revoked commit tx, but we don't check that here, if the channel says it's a revoked + // commit we accept it as such, so it simplifies the test. + val revokedCommitTx = normal.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.copy(txOut = Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey)))) + val dummyClaimMainTx = Transaction(2, Seq(TxIn(OutPoint(revokedCommitTx, 0), Nil, 0)), Seq(revokedCommitTx.txOut.head.copy(amount = 4000 sat)), 0) + val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, Nil), dummyClaimMainTx) + val rcp = RevokedCommitPublished(revokedCommitTx, Some(dummyClaimMain), None, Nil, Nil, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) + DATA_CLOSING(normal.commitments, None, BlockHeight(0), Nil, revokedCommitPublished = List(rcp)) + } + + nodeParams.db.channels.addOrUpdateChannel(upstreamChannel) + nodeParams.db.channels.addOrUpdateChannel(downstreamChannel) + assert(Closing.isClosed(downstreamChannel, None) == None) + + val (_, postRestart) = f.createRelayer(nodeParams) + sender.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs) + val brokenHtlcs = sender.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs] + assert(brokenHtlcs.relayedOut.isEmpty) + assert(brokenHtlcs.notRelayed == htlc_ab.map(htlc => PostRestartHtlcCleaner.IncomingHtlc(htlc.add, None))) + } + test("handle a channel relay htlc-fail") { f => import f._ @@ -464,7 +511,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit register.expectNoMessage(100 millis) sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1)) - register.expectMsg(Register.Forward(ActorRef.noSender, testCase.origin.originChannelId, CMD_FULFILL_HTLC(testCase.origin.originHtlcId, preimage1, commit = true))) + register.expectMsg(Register.Forward(null, testCase.origin.originChannelId, CMD_FULFILL_HTLC(testCase.origin.originHtlcId, preimage1, commit = true))) eventListener.expectMsgType[ChannelPaymentRelayed] sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1)) @@ -482,8 +529,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This downstream HTLC has two upstream HTLCs. sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil - assert(fails.toSet === testCase.origin_1.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) + assert(fails.toSet == testCase.origin_1.htlcs.map { + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) @@ -495,7 +542,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) }.head) register.expectNoMessage(100 millis) @@ -512,8 +559,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This downstream HTLC has two upstream HTLCs. sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil - assert(fulfills.toSet === testCase.origin_1.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) + assert(fulfills.toSet == testCase.origin_1.htlcs.map { + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) }.toSet) sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1)) @@ -522,7 +569,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage. sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.origin_2, preimage2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) }.head) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2)) @@ -541,7 +588,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.origin_2)) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) }.head) sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2)) @@ -674,7 +721,7 @@ object PostRestartHtlcCleanerSpec { val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(randomBytes32()), None)) UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) } @@ -683,17 +730,17 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(randomBytes32()), None)) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) } - def buildForwardFail(add: UpdateAddHtlc, origin: Origin.Cold) = + def buildForwardFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty))) - def buildForwardOnChainFail(add: UpdateAddHtlc, origin: Origin.Cold) = + def buildForwardOnChainFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add)))) - def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin.Cold, preimage: ByteVector32) = + def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin.Cold, preimage: ByteVector32): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill] = RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage))) case class LocalPaymentTest(parentId: UUID, childIds: Seq[UUID], fails: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail]], fulfills: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill]]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala new file mode 100644 index 0000000000..e7cf259e7e --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala @@ -0,0 +1,129 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.payment.receive + +import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import akka.actor.typed.eventstream.EventStream +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.scalacompat.Block +import fr.acinq.eclair.TestDatabases.TestSqliteDatabases +import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, PaymentType, PaymentsDbSpec} +import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.payment.receive.InvoicePurger.{PurgeCompleted, PurgeEvent} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32} +import org.scalatest.funsuite.AnyFunSuiteLike + +import scala.concurrent.duration.DurationInt + +class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with AnyFunSuiteLike { + + import PaymentsDbSpec._ + + test("purge invoices on startup") { + val dbs = TestSqliteDatabases() + val db = dbs.db.payments + val count = 10 + + // create expired invoices + val expiredInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice"), CltvExpiryDelta(18), + timestamp = 1 unixsec)) + val expiredPayments = expiredInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired)) + expiredPayments.foreach(payment => db.addIncomingPayment(payment.invoice, payment.paymentPreimage)) + + // create pending invoices + val pendingInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("pending invoice"), CltvExpiryDelta(18))) + val pendingPayments = pendingInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending)) + pendingPayments.foreach(payment => db.addIncomingPayment(payment.invoice, payment.paymentPreimage)) + + // create paid invoices + val receivedAt = TimestampMilli.now() + 1.milli + val paidInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("paid invoice"), CltvExpiryDelta(18))) + val paidPayments = paidInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(100 msat, receivedAt))) + paidPayments.foreach(payment => { + db.addIncomingPayment(payment.invoice, payment.paymentPreimage) + // receive payment + db.receiveIncomingPayment(payment.invoice.paymentHash, 100 msat, receivedAt) + }) + + val now = TimestampMilli.now() + assert(db.listIncomingPayments(0 unixms, now) == expiredPayments ++ pendingPayments ++ paidPayments) + assert(db.listIncomingPayments(now - 100.days, now) == pendingPayments ++ paidPayments) + assert(db.listPendingIncomingPayments(0 unixms, now) == pendingPayments) + assert(db.listReceivedIncomingPayments(0 unixms, now) == paidPayments) + assert(db.listExpiredIncomingPayments(0 unixms, now) == expiredPayments) + + val probe = testKit.createTestProbe[PurgeEvent]() + system.eventStream ! EventStream.Subscribe(probe.ref) + + val purger = testKit.spawn(InvoicePurger(db, 24.hours), name = "purge-expired-invoices") + + // check that purge runs before the default first interval of 24 hours + probe.expectMessage(5 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, now).isEmpty) + assert(db.listIncomingPayments(0 unixms, now) == pendingPayments ++ paidPayments) + + testKit.stop(purger) + } + + test("purge invoices after interval") { + val dbs = TestSqliteDatabases() + val db = dbs.db.payments + val interval = 5 seconds + + // add an expired invoice from before the 15 days look back period + val expiredInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), + timestamp = 5 unixsec) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment1.invoice, expiredPayment1.paymentPreimage) + + // add an expired invoice from after the 15 day look back period + val expiredInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), + timestamp = TimestampSecond.now() - 10.days) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment2.invoice, expiredPayment2.paymentPreimage) + + val probe = testKit.createTestProbe[PurgeEvent]() + system.eventStream ! EventStream.Subscribe(probe.ref) + + val purger = testKit.spawn(InvoicePurger(db, interval), name = "purge-expired-invoices") + + // check that the initial purge scanned the entire database + probe.expectMessage(10 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()).isEmpty) + + // add an expired invoice from before the 15 days look back period + val expiredInvoice3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice3"), CltvExpiryDelta(18), + timestamp = 5 unixsec) + val expiredPayment3 = IncomingPayment(expiredInvoice3, randomBytes32(), PaymentType.Standard, expiredInvoice3.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment3.invoice, expiredPayment3.paymentPreimage) + + // add another expired invoice from after the 15 day look back period + val expiredInvoice4 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice4"), CltvExpiryDelta(18), + timestamp = TimestampSecond.now() - 10.days) + val expiredPayment4 = IncomingPayment(expiredInvoice4, randomBytes32(), PaymentType.Standard, expiredInvoice4.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment4.invoice, expiredPayment4.paymentPreimage) + + // check that subsequent purge runs do not go back > 15 days + probe.expectMessage(10 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()) == Seq(expiredPayment3)) + + testKit.stop(purger) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 04c8d17387..695a9cf96b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -20,9 +20,11 @@ import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.eclair.Features.ScidAlias import fr.acinq.eclair.TestConstants.emptyOnionPacket import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ @@ -32,7 +34,8 @@ import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, Pa import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayPayload, ChannelRelayTlvPayload, RelayLegacyPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, NodeParams, TestConstants, randomBytes32, _} +import fr.acinq.eclair.{CltvExpiry, NodeParams, RealShortChannelId, TestConstants, randomBytes32, _} +import org.scalatest.Inside.inside import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -57,113 +60,176 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.channelId === channelId) - assert(fwd.message === cmd) + assert(fwd.message == cmd) + assert(fwd.channelId == channelId) fwd } - def expectFwdAdd(register: TestProbe[Any], shortChannelId: ShortChannelId, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.ForwardShortId[CMD_ADD_HTLC] = { - val fwd = register.expectMessageType[Register.ForwardShortId[CMD_ADD_HTLC]] - assert(fwd.shortChannelId === shortChannelId) - assert(fwd.message.amount === outAmount) - assert(fwd.message.cltvExpiry === outExpiry) - assert(fwd.message.origin.isInstanceOf[Origin.ChannelRelayedHot]) - val o = fwd.message.origin.asInstanceOf[Origin.ChannelRelayedHot] - assert(o.amountOut === outAmount) + def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { + val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] + inside(fwd.message) { case add: CMD_ADD_HTLC => + assert(add.amount == outAmount) + assert(add.cltvExpiry == outExpiry) + inside(add.origin) { case o: Origin.ChannelRelayedHot => + assert(o.amountOut == outAmount) + } + } + assert(fwd.channelId == channelId) fwd } - test("relay htlc-add") { f => + def basicRelayTest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(relayPayloadScid, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) - channelRelayer ! WrappedLocalChannelUpdate(u) + channelRelayer ! WrappedLocalChannelUpdate(lcu) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + if (success) { + expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry) + } else { + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) + } + } + + test("relay with real scid (channel update uses real scid)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with real scid (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) + } + + test("relay with local alias (channel update uses real scid)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with local alias (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = false) + } + + test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) + } + + test("relay with local alias when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = true) + } + + test("relay with new real scid after reorg") { f => + import f._ + + // initial channel update + val lcu1 = createLocalUpdate(channelId1) + val payload1 = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r1 = createValidIncomingPacket(payload1) + channelRelayer ! WrappedLocalChannelUpdate(lcu1) + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + + // reorg happens + val realScid1AfterReorg = RealShortChannelId(111112) + val lcu2 = createLocalUpdate(channelId1).modify(_.shortIds.real).setTo(RealScidStatus.Final(realScid1AfterReorg)) + val payload2 = RelayLegacyPayload(realScid1AfterReorg, outgoingAmount, outgoingExpiry) + val r2 = createValidIncomingPacket(payload2) + channelRelayer ! WrappedLocalChannelUpdate(lcu2) + + // both old and new real scids work + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + // new real scid works + channelRelayer ! Relay(r2) + expectFwdAdd(register, lcu2.channelId, outgoingAmount, outgoingExpiry) } - test("relay an htlc-add with onion tlv payload") { f => + test("relay with onion tlv payload") { f => import f._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ - val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(shortId1))) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(realScid1))) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) } - test("relay an htlc-add with retries") { f => + test("relay with retries") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) // we tell the relayer about the first channel - val u1 = createLocalUpdate(shortId1) + val u1 = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u1) // this is another channel, with less balance (it will be preferred) - val u2 = createLocalUpdate(shortId2, 8000000 msat) + val u2 = createLocalUpdate(channelId2, balance = 80_000_000 msat) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) // first try - val fwd1 = expectFwdAdd(register, shortId2, outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScId2), outgoingAmount, outgoingExpiry) // channel returns an error - fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(shortId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(realScId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) // second try - val fwd2 = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd2 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) // failure again - fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(shortId1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) // the relayer should give up expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryNodeFailure), commit = true)) } - test("fail to relay an htlc-add when we have no channel_update for the next channel") { f => + test("fail to relay when we have no channel_update for the next channel") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when register returns an error") { f => + test("fail to relay when register returns an error") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) - fwd.replyTo ! Register.ForwardShortIdFailure(fwd) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) + fwd.replyTo ! Register.ForwardFailure(fwd) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when the channel is advertised as unusable (down)") { f => + test("fail to relay when the channel is advertised as unusable (down)") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val d = LocalChannelDown(null, channelId = channelIds(shortId1), shortId1, outgoingNodeId) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val d = LocalChannelDown(null, channelId1, createShortIds(channelId1), outgoingNodeId) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! WrappedLocalChannelDown(d) @@ -172,12 +238,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add (channel disabled)") { f => + test("fail to relay when channel is disabled") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, enabled = false) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, enabled = false) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -185,12 +251,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (amount below minimum)") { f => + test("fail to relay when amount is below minimum") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, htlcMinimum = outgoingAmount + 1.msat) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, htlcMinimum = outgoingAmount + 1.msat) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -198,25 +264,25 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(outgoingAmount, u.channelUpdate)), commit = true)) } - test("relay an htlc-add (expiry larger than our requirements)") { f => + test("relay when expiry larger than our requirements") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelIds(realScid1), payload.amountToForward, payload.outgoingCltv).message } - test("fail to relay an htlc-add (expiry too small)") { f => + test("fail to relay when expiry is too small") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -224,12 +290,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (fee insufficient)") { f => + test("fail to relay when fee is insufficient") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -237,14 +303,45 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (local error)") { f => + test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u1 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) + + channelRelayer ! WrappedLocalChannelUpdate(u1) + channelRelayer ! Relay(r) + + // relay succeeds with current channel update (u1) with lower fees + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) + + val u2 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 530) + + channelRelayer ! WrappedLocalChannelUpdate(u2) + channelRelayer ! Relay(r) + + // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) + + val u3 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 601) + + channelRelayer ! WrappedLocalChannelUpdate(u1) + channelRelayer ! WrappedLocalChannelUpdate(u3) + channelRelayer ! Relay(r) + + // relay fails because the current update (u3) with higher fees occurred more than 10 minutes ago + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), commit = true)) + } + + test("fail to relay when there is a local error") { f => + import f._ + + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) case class TestCase(exc: ChannelException, update: ChannelUpdate, failure: FailureMessage) @@ -259,7 +356,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), commit = true)) } @@ -269,41 +366,42 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ /** This is just a simplified helper function with random values for fields we are not using here */ - def dummyLocalUpdate(shortChannelId: ShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { + def dummyLocalUpdate(shortChannelId: RealShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { val channelId = randomBytes32() val update = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey(), remoteNodeId, shortChannelId, CltvExpiryDelta(10), 100 msat, 1000 msat, 100, capacity.toMilliSatoshi) - val commitments = PaymentPacketSpec.makeCommitments(ByteVector32.Zeroes, availableBalanceForSend, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, remoteNodeId, None, update, commitments) + val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity) + val shortIds = ShortIds(real = RealScidStatus.Final(shortChannelId), localAlias = ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + LocalChannelUpdate(null, channelId, shortIds, remoteNodeId, None, update, commitments) } val (a, b) = (randomKey().publicKey, randomKey().publicKey) val channelUpdates = Map( - ShortChannelId(11111) -> dummyLocalUpdate(ShortChannelId(11111), a, 100000000 msat, 200000 sat), - ShortChannelId(12345) -> dummyLocalUpdate(ShortChannelId(12345), a, 10000000 msat, 200000 sat), - ShortChannelId(22222) -> dummyLocalUpdate(ShortChannelId(22222), a, 10000000 msat, 100000 sat), - ShortChannelId(22223) -> dummyLocalUpdate(ShortChannelId(22223), a, 9000000 msat, 50000 sat), - ShortChannelId(33333) -> dummyLocalUpdate(ShortChannelId(33333), a, 100000 msat, 50000 sat), - ShortChannelId(44444) -> dummyLocalUpdate(ShortChannelId(44444), b, 1000000 msat, 10000 sat), + ShortChannelId(11111) -> dummyLocalUpdate(RealShortChannelId(11111), a, 100000000 msat, 200000 sat), + ShortChannelId(12345) -> dummyLocalUpdate(RealShortChannelId(12345), a, 10000000 msat, 200000 sat), + ShortChannelId(22222) -> dummyLocalUpdate(RealShortChannelId(22222), a, 10000000 msat, 100000 sat), + ShortChannelId(22223) -> dummyLocalUpdate(RealShortChannelId(22223), a, 9000000 msat, 50000 sat), + ShortChannelId(33333) -> dummyLocalUpdate(RealShortChannelId(33333), a, 100000 msat, 50000 sat), + ShortChannelId(44444) -> dummyLocalUpdate(RealShortChannelId(44444), b, 1000000 msat, 10000 sat), ) channelUpdates.values.foreach(u => channelRelayer ! WrappedLocalChannelUpdate(u)) { val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) // select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment - val cmd1 = expectFwdAdd(register, ShortChannelId(22223), payload.amountToForward, payload.outgoingCltv).message + val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd1.replyTo ! RES_ADD_FAILED(cmd1, ChannelUnavailable(randomBytes32()), None) // select 2nd-to-best channel: higher capacity and balance - val cmd2 = expectFwdAdd(register, ShortChannelId(22222), payload.amountToForward, payload.outgoingCltv).message + val cmd2 = expectFwdAdd(register, channelUpdates(ShortChannelId(22222)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd2.replyTo ! RES_ADD_FAILED(cmd2, TooManyAcceptedHtlcs(randomBytes32(), 42), Some(channelUpdates(ShortChannelId(22222)).channelUpdate)) // select 3rd-to-best channel: same balance but higher capacity - val cmd3 = expectFwdAdd(register, ShortChannelId(12345), payload.amountToForward, payload.outgoingCltv).message + val cmd3 = expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd3.replyTo ! RES_ADD_FAILED(cmd3, TooManyAcceptedHtlcs(randomBytes32(), 42), Some(channelUpdates(ShortChannelId(12345)).channelUpdate)) // select 4th-to-best channel: same capacity but higher balance - val cmd4 = expectFwdAdd(register, ShortChannelId(11111), payload.amountToForward, payload.outgoingCltv).message + val cmd4 = expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), UInt64(100000000), 100000000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) // all the suitable channels have been tried expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) @@ -311,35 +409,35 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) val payload = RelayLegacyPayload(ShortChannelId(12345), 50000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(11111), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // lower amount payment val payload = RelayLegacyPayload(ShortChannelId(12345), 1000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(33333), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // payment too high, no suitable channel found, we keep the requested one val payload = RelayLegacyPayload(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1010000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1010000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(12345), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry larger than our requirements val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(50)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(22223), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry too small, no suitable channel found val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(CltvExpiry(61), channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) } @@ -348,11 +446,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a test("settlement failure") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) @@ -368,7 +466,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) expectFwdFail(register, r.add.channelId, testCase.cmd) @@ -380,10 +478,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val eventListener = TestProbe[ChannelPaymentRelayed]() system.eventStream ! EventStream.Subscribe(eventListener.ref) - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult) @@ -397,17 +495,17 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd1 = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) val fwd2 = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd2.channelId === r.add.channelId) - assert(fwd2.message.id === r.add.id) - assert(fwd2.message.r === paymentPreimage) + assert(fwd2.channelId == r.add.channelId) + assert(fwd2.message.id == r.add.id) + assert(fwd2.message.r == paymentPreimage) val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] - assert(paymentRelayed.copy(timestamp = 0 unixms) === ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0 unixms)) + assert(paymentRelayed.copy(timestamp = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0 unixms)) } } @@ -416,6 +514,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ val channelId_ab = randomBytes32() val channelId_bc = randomBytes32() + val shortIds_ab = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_ab.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) val a = PaymentPacketSpec.a val sender = TestProbe[Relayer.OutgoingChannels]() @@ -426,74 +526,82 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channels } - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) val channels1 = getOutgoingChannels(true) - assert(channels1.size === 2) - assert(channels1.head.channelUpdate === channelUpdate_ab) - assert(channels1.head.toUsableBalance === Relayer.UsableBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false)) - assert(channels1.last.channelUpdate === channelUpdate_bc) - assert(channels1.last.toUsableBalance === Relayer.UsableBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false)) + assert(channels1.size == 2) + assert(channels1.head.channelUpdate == channelUpdate_ab) + assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, shortIds_ab, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) + assert(channels1.last.channelUpdate == channelUpdate_bc) + assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, shortIds_bc, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, shortIds_ab, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) - assert(channels2.last.commitments.availableBalanceForReceive === 500000.msat && channels2.last.commitments.availableBalanceForSend === 200000.msat) + assert(channels2.last.commitments.availableBalanceForReceive == 500000.msat && channels2.last.commitments.availableBalanceForSend == 200000.msat) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, shortIds_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, shortIds_ab, c)) val channels3 = getOutgoingChannels(true) - assert(channels3.size === 1 && channels3.head.commitments.availableBalanceForSend === 100000.msat) + assert(channels3.size == 1 && channels3.head.commitments.availableBalanceForSend == 100000.msat) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels4 = getOutgoingChannels(true) assert(channels4.isEmpty) val channels5 = getOutgoingChannels(false) - assert(channels5.size === 1) + assert(channels5.size == 1) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels6 = getOutgoingChannels(true) - assert(channels6.size === 1) - - // Simulate a chain re-org that changes the shortChannelId: - channelRelayer ! WrappedShortChannelIdAssigned(ShortChannelIdAssigned(null, channelId_ab, ShortChannelId(42), Some(channelUpdate_ab.shortChannelId))) - val channels7 = getOutgoingChannels(true) - assert(channels7.isEmpty) - - // We should receive the updated channel update containing the new shortChannelId: - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, ShortChannelId(42), a, None, channelUpdate_ab.copy(shortChannelId = ShortChannelId(42)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - val channels8 = getOutgoingChannels(true) - assert(channels8.size === 1) - assert(channels8.head.channelUpdate.shortChannelId === ShortChannelId(42)) + assert(channels6.size == 1) } } object ChannelRelayerSpec { - val paymentPreimage = randomBytes32() - val paymentHash = Crypto.sha256(paymentPreimage) + val paymentPreimage: ByteVector32 = randomBytes32() + val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) + + val outgoingAmount: MilliSatoshi = 10_000_000 msat + val outgoingExpiry: CltvExpiry = CltvExpiry(400000) + val outgoingNodeId: PublicKey = randomKey().publicKey + + val realScid1: RealShortChannelId = RealShortChannelId(111111) + val realScId2: RealShortChannelId = RealShortChannelId(222222) - val outgoingAmount = 1000000 msat - val outgoingExpiry = CltvExpiry(400000) - val outgoingNodeId = randomKey().publicKey + val localAlias1: Alias = Alias(111000) + val localAlias2: Alias = Alias(222000) - val shortId1 = ShortChannelId(111111) - val shortId2 = ShortChannelId(222222) + val channelId1: ByteVector32 = randomBytes32() + val channelId2: ByteVector32 = randomBytes32() val channelIds = Map( - shortId1 -> randomBytes32(), - shortId2 -> randomBytes32() + realScid1 -> channelId1, + realScId2 -> channelId2, + localAlias1 -> channelId1, + localAlias2 -> channelId2, ) - def createValidIncomingPacket(amountIn: MilliSatoshi, expiryIn: CltvExpiry, payload: ChannelRelayPayload): IncomingPaymentPacket.ChannelRelayPacket = { + def createValidIncomingPacket(payload: ChannelRelayPayload, amountIn: MilliSatoshi = 11_000_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100)): IncomingPaymentPacket.ChannelRelayPacket = { val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket) ChannelRelayPacket(add_ab, payload, emptyOnionPacket) } - def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat): LocalChannelUpdate = { - val channelId = channelIds(shortChannelId) - val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, 0 unixsec, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, 1000 msat, 100, Some(capacity.toMilliSatoshi)) - val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, outgoingNodeId, None, update, commitments) + def createShortIds(channelId: ByteVector32) = { + val realScid = channelIds.collectFirst { case (realScid: RealShortChannelId, cid) if cid == channelId => realScid }.get + val localAlias = channelIds.collectFirst { case (localAlias: Alias, cid) if cid == channelId => localAlias }.get + ShortIds(real = RealScidStatus.Final(realScid), localAlias, remoteAlias_opt = None) + } + + def createLocalUpdate(channelId: ByteVector32, channelUpdateScid_opt: Option[ShortChannelId] = None, balance: MilliSatoshi = 100_000_000 msat, capacity: Satoshi = 5_000_000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100, optionScidAlias: Boolean = false): LocalChannelUpdate = { + val shortIds = createShortIds(channelId) + val channelUpdateScid = channelUpdateScid_opt.getOrElse(shortIds.real.toOption.get) + val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, channelUpdateScid, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) + val features: Set[PermanentChannelFeature] = Set( + if (optionScidAlias) Some(ScidAlias) else None, + ).flatten + val channelFeatures = ChannelFeatures(features) + val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity, channelFeatures = channelFeatures) + LocalChannelUpdate(null, channelId, shortIds, outgoingNodeId, None, update, commitments) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index b80b16f0b7..a89702934d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -23,13 +23,14 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter._ import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.NodeRelayer.PaymentKey import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} @@ -38,7 +39,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.router.Router.RouteRequest import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -101,26 +102,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(payment1) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending1.keySet === Set(PaymentKey(paymentHash1, paymentSecret1))) + assert(pending1.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)))) val (paymentHash2, paymentSecret2) = (randomBytes32(), randomBytes32()) val payment2 = createPartialIncomingPacket(paymentHash2, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment2) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending2.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2))) + assert(pending2.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)))) val payment3a = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3a) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending3 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending3.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending3.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)), PaymentKey(paymentHash1, Some(paymentSecret2)))) val payment3b = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3b) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending4 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending4.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending4.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)), PaymentKey(paymentHash1, Some(paymentSecret2)))) register.expectNoMessage(100 millis) } @@ -136,8 +137,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl probe.expectMessage(Map.empty) } { - val (paymentHash1, paymentSecret1, child1) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) - val (paymentHash2, paymentSecret2, child2) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) + val (paymentHash1, paymentSecret1, child1) = (randomBytes32(), Some(randomBytes32()), TestProbe[NodeRelay.Command]()) + val (paymentHash2, paymentSecret2, child2) = (randomBytes32(), Some(randomBytes32()), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash1, paymentSecret1) -> child1.ref, PaymentKey(paymentHash2, paymentSecret2) -> child2.ref) val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) @@ -152,8 +153,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl } { val paymentHash = randomBytes32() - val (paymentSecret1, child1) = (randomBytes32(), TestProbe[NodeRelay.Command]()) - val (paymentSecret2, child2) = (randomBytes32(), TestProbe[NodeRelay.Command]()) + val (paymentSecret1, child1) = (Some(randomBytes32()), TestProbe[NodeRelay.Command]()) + val (paymentSecret2, child2) = (Some(randomBytes32()), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash, paymentSecret1) -> child1.ref, PaymentKey(paymentHash, paymentSecret2) -> child2.ref) val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) @@ -169,18 +170,18 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending1.size === 1) - assert(pending1.head._1 === PaymentKey(paymentHash, incomingSecret)) + assert(pending1.size == 1) + assert(pending1.head._1 == PaymentKey(paymentHash, Some(incomingSecret))) - parentRelayer ! NodeRelayer.RelayComplete(pending1.head._2, paymentHash, incomingSecret) + parentRelayer ! NodeRelayer.RelayComplete(pending1.head._2, paymentHash, Some(incomingSecret)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(Map.empty) parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending2.size === 1) - assert(pending2.head._1 === pending1.head._1) + assert(pending2.size == 1) + assert(pending2.head._1 == pending1.head._1) assert(pending2.head._2 !== pending1.head._2) } } @@ -194,9 +195,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // after a while the payment times out incomingMultiPart.dropRight(1).foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) - assert(fwd.channelId === p.add.channelId) + assert(fwd.channelId == p.add.channelId) val failure = Right(PaymentTimeout) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] @@ -212,16 +213,16 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // and then one extra val extra = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(extra) // the extra payment will be rejected val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === extra.add.channelId) + assert(fwd.channelId == extra.add.channelId) val failure = IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight) - assert(fwd.message === CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) register.expectNoMessage(100 millis) } @@ -241,28 +242,28 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive new extraneous multi-part HTLC. val i1 = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(i1) val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd1.channelId === i1.add.channelId) + assert(fwd1.channelId == i1.add.channelId) val failure1 = IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight) - assert(fwd1.message === CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) + assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) // Receive new HTLC with different details, but for the same payment hash. val i2 = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(i2) val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd1.channelId === i1.add.channelId) + assert(fwd1.channelId == i1.add.channelId) val failure2 = IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight) - assert(fwd2.message === CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) + assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) register.expectNoMessage(100 millis) } @@ -277,8 +278,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) register.expectNoMessage(100 millis) } @@ -293,8 +294,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) register.expectNoMessage(100 millis) } @@ -314,8 +315,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) } register.expectNoMessage(100 millis) @@ -329,8 +330,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) register.expectNoMessage(100 millis) } @@ -347,8 +348,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -362,8 +363,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) register.expectNoMessage(100 millis) } @@ -380,8 +381,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) } register.expectNoMessage(100 millis) @@ -402,8 +403,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -427,8 +428,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incoming.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)) } register.expectNoMessage(100 millis) @@ -450,8 +451,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -472,8 +473,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) } register.expectNoMessage(100 millis) @@ -488,9 +489,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val routeRequest = router.expectMessageType[RouteRequest] val routeParams = routeRequest.routeParams - assert(routeParams.boundaries.maxFeeProportional === 0) // should be disabled - assert(routeParams.boundaries.maxFeeFlat === incomingAmount - outgoingAmount) - assert(routeParams.boundaries.maxCltv === incomingSinglePart.add.cltvExpiry - outgoingExpiry) + assert(routeParams.boundaries.maxFeeProportional == 0) // should be disabled + assert(routeParams.boundaries.maxFeeFlat == incomingAmount - outgoingAmount) + assert(routeParams.boundaries.maxCltv == incomingSinglePart.add.cltvExpiry - outgoingExpiry) assert(routeParams.includeLocalChannelCost) } @@ -515,8 +516,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } // If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream. @@ -527,7 +528,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming.toSet === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet) + assert(relayEvent.incoming.toSet == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -550,13 +551,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) val incomingAdd = incomingSinglePart.add val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === incomingAdd.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)) + assert(fwd.channelId == incomingAdd.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)) nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId))) + assert(relayEvent.incoming == Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId))) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -566,11 +567,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features) + val hints = List(ExtraHop(randomKey().publicKey, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) + val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( - incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr + incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice ))) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) @@ -578,27 +579,32 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret - assert(outgoingPayment.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata - assert(outgoingPayment.totalAmount === outgoingAmount) - assert(outgoingPayment.targetExpiry === outgoingExpiry) - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.additionalTlvs === Nil) - assert(outgoingPayment.assistedRoutes === hints) + assert(outgoingPayment.paymentSecret == invoice.paymentSecret) // we should use the provided secret + assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.totalAmount == outgoingAmount) + assert(outgoingPayment.targetExpiry == outgoingExpiry) + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.additionalTlvs == Nil) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) + assert(relayEvent.incoming == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -608,11 +614,11 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456")) - assert(!pr.features.allowMultiPart) + val hints = List(ExtraHop(randomKey().publicKey, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456")) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( - incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr + incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice ))) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) @@ -620,26 +626,32 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] - assert(outgoingPayment.finalPayload.amount === outgoingAmount) - assert(outgoingPayment.finalPayload.expiry === outgoingExpiry) - assert(outgoingPayment.finalPayload.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.assistedRoutes === hints) + assert(outgoingPayment.finalPayload.amount == outgoingAmount) + assert(outgoingPayment.finalPayload.expiry == outgoingExpiry) + assert(outgoingPayment.finalPayload.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) + // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) - assert(relayEvent.outgoing.length === 1) + assert(relayEvent.incoming == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) + assert(relayEvent.outgoing.length == 1) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) } @@ -648,9 +660,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18)) val incomingPayments = incomingMultiPart.map(incoming => { - val innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr) + val innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice) val invalidPayload = innerPayload.copy(records = TlvStream(innerPayload.records.records.collect { case r if !r.isInstanceOf[OnionPaymentPayloadTlv.PaymentData] => r })) // we remove the payment secret incoming.copy(innerPayload = invalidPayload) }) @@ -659,34 +671,34 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true)) } } def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream): Unit = { assert(!outgoingCfg.publishEvent) assert(!outgoingCfg.storeInDb) - assert(outgoingCfg.paymentHash === paymentHash) - assert(outgoingCfg.paymentRequest === None) - assert(outgoingCfg.recipientAmount === outgoingAmount) - assert(outgoingCfg.recipientNodeId === outgoingNodeId) - assert(outgoingCfg.upstream === upstream) + assert(outgoingCfg.paymentHash == paymentHash) + assert(outgoingCfg.invoice == None) + assert(outgoingCfg.recipientAmount == outgoingAmount) + assert(outgoingCfg.recipientNodeId == outgoingNodeId) + assert(outgoingCfg.upstream == upstream) } def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = { - assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret - assert(outgoingPayment.totalAmount === outgoingAmount) - assert(outgoingPayment.targetExpiry === outgoingExpiry) - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.additionalTlvs === Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) - assert(outgoingPayment.assistedRoutes === Nil) + assert(!outgoingPayment.paymentSecret.contains(incomingSecret)) // we should generate a new outgoing secret + assert(outgoingPayment.totalAmount == outgoingAmount) + assert(outgoingPayment.targetExpiry == outgoingExpiry) + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.additionalTlvs == Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) + assert(outgoingPayment.extraEdges == Nil) } def validateRelayEvent(e: TrampolinePaymentRelayed): Unit = { - assert(e.amountIn === incomingAmount) + assert(e.amountIn == incomingAmount) assert(e.amountOut >= outgoingAmount) // outgoingAmount + routing fees - assert(e.paymentHash === paymentHash) + assert(e.paymentHash == paymentHash) } } @@ -704,7 +716,8 @@ object NodeRelayerSpec { val outgoingAmount = 4000000 msat val outgoingExpiry = CltvExpiry(490000) - val outgoingNodeId = randomKey().publicKey + val outgoingNodeKey = randomKey() + val outgoingNodeId = outgoingNodeKey.publicKey val incomingAmount = 5000000 msat val incomingSecret = randomBytes32() @@ -721,9 +734,9 @@ object NodeRelayerSpec { def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = { val outerPayload = if (amountIn == totalAmountIn) { - PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret, None) + PaymentOnion.createSinglePartPayload(amountIn, expiryIn, Some(incomingSecret), None) } else { - PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None) + PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, Some(incomingSecret), None) } IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), @@ -737,7 +750,7 @@ object NodeRelayerSpec { val amountIn = incomingAmount / 2 IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None), + PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, Some(paymentSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId), nextTrampolinePacket) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 98b7e019a4..1e041e8674 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -22,7 +22,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.{TypedActorContextOps, TypedActorRefOps} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.FinalPacket @@ -30,7 +30,8 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.{OutgoingPaymentPacket, PaymentPacketSpec} -import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate +import fr.acinq.eclair.router.Router.NodeHop import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} import org.scalatest.concurrent.PatienceConfiguration @@ -77,7 +78,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat assert(sender.expectMessageType[Relayer.OutgoingChannels].channels.isEmpty) // We publish a channel update, that should be picked up by the channel relayer - system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) eventually(PatienceConfiguration.Timeout(30 seconds), PatienceConfiguration.Interval(1 second)) { childActors.channelRelayer ! ChannelRelayer.GetOutgoingChannels(sender.ref.toClassic, GetOutgoingChannels()) val channels = sender.expectMessageType[Relayer.OutgoingChannels].channels @@ -85,24 +87,24 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat } // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) - register.expectMessageType[Register.ForwardShortId[CMD_ADD_HTLC]] + register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] } test("relay an htlc-add at the final node to the payment handler") { f => import f._ - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) val fp = paymentHandler.expectMessageType[FinalPacket] - assert(fp.add === add_ab) - assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + assert(fp.add == add_ab) + assert(fp.payload == PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) register.expectNoMessage(50 millis) } @@ -115,22 +117,22 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // We simulate a payment split between multiple trampoline routes. val totalAmount = finalAmount * 3 val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None)) - assert(trampolineAmount === finalAmount) - assert(trampolineExpiry === finalExpiry) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) - assert(cmd.amount === finalAmount) - assert(cmd.cltvExpiry === finalExpiry) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, Some(paymentSecret), None)) + assert(trampolineAmount == finalAmount) + assert(trampolineExpiry == finalExpiry) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + assert(cmd.amount == finalAmount) + assert(cmd.cltvExpiry == finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) val fp = paymentHandler.expectMessageType[FinalPacket] - assert(fp.add === add_ab) - assert(fp.payload.amount === finalAmount) - assert(fp.payload.totalAmount === totalAmount) - assert(fp.payload.expiry === finalExpiry) - assert(fp.payload.paymentSecret === paymentSecret) + assert(fp.add == add_ab) + assert(fp.payload.amount == finalAmount) + assert(fp.payload.totalAmount == totalAmount) + assert(fp.payload.expiry == finalExpiry) + assert(fp.payload.paymentSecret contains paymentSecret) register.expectNoMessage(50 millis) } @@ -139,16 +141,16 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) // and then manually build an htlc with an invalid onion (hmac) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) relayer ! RelayForward(add_ab) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id == add_ab.id) assert(fail.onionHash == Sphinx.hash(add_ab.onionRoutingPacket)) - assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) + assert(fail.failureCode == (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) register.expectNoMessage(50 millis) } @@ -160,8 +162,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use this to build a valid trampoline onion inside a normal onion val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) @@ -169,7 +171,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat relayer ! RelayForward(add_ab) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id == add_ab.id) assert(fail.reason == Right(RequiredNodeFeatureMissing)) register.expectNoMessage(50 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index 80dba79fc3..b5d7775a4f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -19,16 +19,16 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.pattern.pipe import akka.testkit.TestProbe -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, Satoshi, SatoshiLong, Script, Transaction} +import sttp.client3.okhttp.OkHttpFutureBackend +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, Satoshi, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.ValidateResult import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, randomKey} import org.json4s.JsonAST.JString import org.scalatest.funsuite.AnyFunSuite @@ -64,10 +64,10 @@ class AnnouncementsBatchValidationSpec extends AnyFunSuite { bitcoinClient.validate(announcements(0)).pipeTo(sender.ref) sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(1).copy(shortChannelId = ShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height + bitcoinClient.validate(announcements(1).copy(shortChannelId = RealShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(2).copy(shortChannelId = ShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index + bitcoinClient.validate(announcements(2).copy(shortChannelId = RealShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index sender.expectMsgType[ValidateResult].fundingTx.isRight } @@ -101,7 +101,7 @@ object AnnouncementsBatchValidationSpec { def makeChannelAnnouncement(c: SimulatedChannel, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): ChannelAnnouncement = { val (blockHeight, txIndex) = Await.result(bitcoinClient.getTransactionShortId(c.fundingTx.txid), 10 seconds) - val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) + val shortChannelId = RealShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key.publicKey, c.node2Key.publicKey, c.node1FundingKey.publicKey, c.node2FundingKey.publicKey, Features.empty) val channelAnnNodeSig1 = Announcements.signChannelAnnouncement(witness, c.node1Key) val channelAnnBitcoinSig1 = Announcements.signChannelAnnouncement(witness, c.node1FundingKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index e1b1e4eced..b82360bc0f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -16,13 +16,15 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Block -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Block +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags -import fr.acinq.eclair.wire.protocol.NodeAddress +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.nodeAnnouncementCodec +import fr.acinq.eclair.wire.protocol.{Color, NodeAddress} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -42,14 +44,14 @@ class AnnouncementsSpec extends AnyFunSuite { test("create valid signed channel announcement") { val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey(), randomKey(), randomKey(), randomKey()) - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) val node_a_sig = Announcements.signChannelAnnouncement(witness, node_a) val bitcoin_a_sig = Announcements.signChannelAnnouncement(witness, bitcoin_a) val node_b_sig = Announcements.signChannelAnnouncement(witness, node_b) val bitcoin_b_sig = Announcements.signChannelAnnouncement(witness, bitcoin_b) - val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) + val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) assert(checkSigs(ann)) - assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) === false) + assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) == false) } test("create valid signed node announcement") { @@ -63,9 +65,9 @@ class AnnouncementsSpec extends AnyFunSuite { Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentMetadata -> FeatureSupport.Optional, ) - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures()) // Features should be filtered to only include node_announcement related features. - assert(ann.features === Features( + assert(ann.features == Features( Features.DataLossProtect -> FeatureSupport.Optional, Features.ChannelRangeQueries -> FeatureSupport.Optional, Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional, @@ -74,7 +76,7 @@ class AnnouncementsSpec extends AnyFunSuite { Features.BasicMultiPartPayment -> FeatureSupport.Optional, )) assert(checkSig(ann)) - assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false) + assert(checkSig(ann.copy(timestamp = 153 unixsec)) == false) } test("sort node announcement addresses") { @@ -84,9 +86,9 @@ class AnnouncementsSpec extends AnyFunSuite { NodeAddress.fromParts("140.82.121.4", 9735).get, NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, ) - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures()) assert(checkSig(ann)) - assert(ann.addresses === List( + assert(ann.addresses == List( NodeAddress.fromParts("140.82.121.4", 9735).get, NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, @@ -95,13 +97,13 @@ class AnnouncementsSpec extends AnyFunSuite { } test("nodeParams.nodeId equals nodeParams.privateKey.publicKey") { - assert(Alice.nodeParams.nodeId === Alice.nodeParams.privateKey.publicKey) + assert(Alice.nodeParams.nodeId == Alice.nodeParams.privateKey.publicKey) } test("create valid signed channel update announcement") { - val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDelta, Alice.nodeParams.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) + val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.channelConf.expiryDelta, Alice.nodeParams.channelConf.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) assert(checkSig(ann, Alice.nodeParams.nodeId)) - assert(checkSig(ann, randomKey().publicKey) === false) + assert(checkSig(ann, randomKey().publicKey) == false) } test("check flags") { @@ -128,4 +130,12 @@ class AnnouncementsSpec extends AnyFunSuite { assert(!channelUpdate2_disabled.channelFlags.isEnabled) } + test("announce irrelevant features") { + // This announcement has option_payment_metadata which is not a node feature + val encoded = hex"25d8bf19e2c6562a85b3109122118a50728d42c9054537b5a26c1e982407f0923a6f162cf0939bfaf58d7e7a7a7c86d7dfd02dda9b0d2036e8fd35a7afc78daa00070100000000000061fcfc1d039093816bb908c043341f3ee47ea86179627d26aa00704593503e684a0b9a38cb01020374657374206e6f646500000000000000000000000000000000000000000000000007018c5279042607" + val ann = nodeAnnouncementCodec.decode(encoded.bits).require.value + assert(ann.features.hasFeature(Features.PaymentMetadata)) + assert(checkSig(ann)) + assert(nodeAnnouncementCodec.encode(ann).require.bytes == encoded) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala new file mode 100644 index 0000000000..8c4e6371c4 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala @@ -0,0 +1,292 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.router + +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} +import fr.acinq.eclair.payment.Invoice +import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelRelayParams} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomKey} +import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper +import org.scalatest.funsuite.AnyFunSuite + +import scala.concurrent.duration.DurationInt + +class BalanceEstimateSpec extends AnyFunSuite { + + def isValid(balance: BalanceEstimate): Boolean = { + balance.low >= 0.msat && + balance.low <= balance.high && + balance.high <= balance.maxCapacity + } + + def makeEdge(nodeId1: PublicKey, nodeId2: PublicKey, channelId: Long, capacity: Satoshi): GraphEdge = + GraphEdge( + ChannelDesc(ShortChannelId(channelId), nodeId1, nodeId2), + ChannelRelayParams.FromHint(Invoice.BasicEdge(nodeId1, nodeId2, ShortChannelId(channelId), 0 msat, 0, CltvExpiryDelta(0))), + capacity, None) + + def makeEdge(channelId: Long, capacity: Satoshi): GraphEdge = + makeEdge(randomKey().publicKey, randomKey().publicKey, channelId, capacity) + + test("no balance information") { + val balance = BalanceEstimate.empty(1 day).addEdge(makeEdge(0, 100 sat)) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(25000 msat) === 0.75 +- 0.001) + assert(balance.canSend(50000 msat) === 0.5 +- 0.001) + assert(balance.canSend(75000 msat) === 0.25 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("add and remove channels") { + val a = makeEdge(0, 200 sat) + val b = makeEdge(1, 100 sat) + val c = makeEdge(2, 800 sat) + val d = makeEdge(3, 120 sat) + val e = makeEdge(4, 190 sat) + val balance = BalanceEstimate.empty(1 day) + .addEdge(a) + .addEdge(b) + .removeEdge(a.desc) + assert(isValid(balance)) + assert(balance.maxCapacity == 100.sat) + val balance1 = balance + .addEdge(c) + .removeEdge(c.desc) + .removeEdge(b.desc) + .addEdge(d) + .addEdge(e) + assert(isValid(balance1)) + assert(balance1.maxCapacity == 190.sat) + val balance2 = balance1 + .removeEdge(d.desc) + .removeEdge(e.desc) + assert(isValid(balance2)) + assert(balance2.maxCapacity == 0.sat) + assert(balance2.capacities.isEmpty) + } + + test("update bounds based on what could then could not be sent (increasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 100 sat)) + .couldSend(24000 msat, now) + .couldNotSend(30000 msat, now) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(23999 msat) === 1.0 +- 0.001) + assert(balance.canSend(24000 msat) === 1.0 +- 0.001) + assert(balance.canSend(24001 msat) === 1.0 +- 0.001) + assert(balance.canSend(27000 msat) === 0.5 +- 0.001) + assert(balance.canSend(29999 msat) === 0.0 +- 0.001) + assert(balance.canSend(30000 msat) === 0.0 +- 0.001) + assert(balance.canSend(30001 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could then could not be sent (decreasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 75 sat)) + .addEdge(makeEdge(1, 100 sat)) + .couldSend(26000 msat, now) + .couldNotSend(14000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(7000 msat) === 0.5 +- 0.001) + assert(balance.canSend(14000 msat) === 0.0 +- 0.001) + assert(balance.canSend(26000 msat) === 0.0 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could not then could be sent (increasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 50 sat)) + .couldNotSend(26000 msat, now) + .couldSend(30000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(30000 msat) === 1.0 +- 0.001) + assert(balance.canSend(65000 msat) === 0.5 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could not then could be sent (decreasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 50 sat)) + .couldNotSend(30000 msat, now) + .couldSend(20000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(20000 msat) === 1.0 +- 0.001) + assert(balance.canSend(25000 msat) === 0.5 +- 0.001) + assert(balance.canSend(30000 msat) === 0.0 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("decay restores baseline bounds") { + val longAgo = TimestampSecond.now() - 30.seconds + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(32000 msat, longAgo) + .couldSend(28000 msat, longAgo) + assert(isValid(balance)) + assert(balance.canSend(1 msat) === 1.0 +- 0.01) + assert(balance.canSend(33333 msat) === 0.666 +- 0.01) + assert(balance.canSend(66666 msat) === 0.333 +- 0.01) + assert(balance.canSend(99999 msat) === 0.0 +- 0.01) + } + + test("sending on single channel shifts amounts") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(80000 msat) === 0.0 +- 0.001) + val balanceAfterSend = balance.didSend(20000 msat, now) + assert(isValid(balanceAfterSend)) + assert(balanceAfterSend.canSend(30000 msat) === 1.0 +- 0.001) + assert(balanceAfterSend.canSend(45000 msat) === 0.5 +- 0.001) + assert(balanceAfterSend.canSend(60000 msat) === 0.0 +- 0.001) + } + + test("sending on single channel after decay") { + val longAgo = TimestampSecond.now() - 60.seconds + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, longAgo) + .couldSend(50000 msat, longAgo) + .didSend(40000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.01) + assert(balance.canSend(10000 msat) <= 0.9) + assert(balance.canSend(50000 msat) >= 0.1) + assert(balance.canSend(60000 msat) === 0.0 +- 0.01) + } + + test("sending on parallel channels shifts low only") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 80 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(80000 msat) === 0.0 +- 0.001) + val balanceAfterSend = balance.didSend(20000 msat, now) + assert(isValid(balanceAfterSend)) + assert(balanceAfterSend.canSend(30000 msat) === 1.0 +- 0.001) + assert(balanceAfterSend.canSend(70000 msat) > 0.1) + } + + test("receiving on single channel shifts amounts") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + .didReceive(10000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(60000 msat) === 1.0 +- 0.001) + assert(balance.canSend(75000 msat) === 0.5 +- 0.001) + assert(balance.canSend(90000 msat) === 0.0 +- 0.001) + } + + test("receiving on single channel after decay") { + val longAgo = TimestampSecond.now() - 60.seconds + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, longAgo) + .couldSend(50000 msat, longAgo) + .didReceive(10000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(10000 msat) >= 0.9) + assert(balance.canSend(20000 msat) <= 0.9) + assert(balance.canSend(80000 msat) >= 0.1) + assert(balance.canSend(90000 msat) <= 0.1) + } + + test("receiving on parallel channels shifts high only") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 80 sat)) + .couldNotSend(70000 msat, now) + .couldSend(50000 msat, now) + .didReceive(20000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(70000 msat) === 0.5 +- 0.001) + assert(balance.canSend(90000 msat) === 0.0 +- 0.001) + } + + test("baseline from graph") { + val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) + val g = DirectedGraph(Seq( + makeEdge(a, b, 1, 100 sat), + makeEdge(b, a, 1, 100 sat), + makeEdge(a, b, 2, 110 sat), + makeEdge(b, a, 3, 120 sat), + makeEdge(a, c, 4, 130 sat), + makeEdge(c, a, 4, 130 sat), + makeEdge(a, c, 5, 140 sat), + makeEdge(c, a, 5, 140 sat), + makeEdge(b, c, 6, 150 sat), + )) + + val graphWithBalances = GraphWithBalanceEstimates(g, 1 day) + // NB: it doesn't matter which edge is selected, the balance estimation takes all existing edges into account. + val edge_ab = makeEdge(a, b, 1, 10 sat) + val edge_ba = makeEdge(b, a, 1, 10 sat) + val edge_bc = makeEdge(b, c, 6, 10 sat) + assert(graphWithBalances.canSend(27500 msat, edge_ab) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(55000 msat, edge_ab) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(30000 msat, edge_ba) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(60000 msat, edge_ba) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(75000 msat, edge_bc) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(100000 msat, edge_bc) === 0.33 +- 0.01) + val unknownEdge = makeEdge(42, 40 sat) + assert(graphWithBalances.canSend(10000 msat, unknownEdge) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(20000 msat, unknownEdge) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(30000 msat, unknownEdge) === 0.25 +- 0.01) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index bebcd2e205..6cc08c32b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -19,12 +19,12 @@ package fr.acinq.eclair.router import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortChannelIdAssigned, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.Peer.PeerRoutingMessage @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{TestKitBaseClass, randomKey, _} +import fr.acinq.eclair._ import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.ByteVector @@ -66,38 +66,46 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // in the tests we are 'a', we don't define a node_a, it will be generated automatically when the router validates the first channel val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty) val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty) val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty) - val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val channelId_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val channelId_ef = ShortChannelId(BlockHeight(420000), 4, 0) - val channelId_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) - val channelId_gh = ShortChannelId(BlockHeight(420000), 6, 0) - - val chan_ab = channelAnnouncement(channelId_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) - val chan_bc = channelAnnouncement(channelId_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) - val chan_cd = channelAnnouncement(channelId_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) - val chan_ef = channelAnnouncement(channelId_ef, priv_e, priv_f, priv_funding_e, priv_funding_f) - val chan_gh = channelAnnouncement(channelId_gh, priv_g, priv_h, priv_funding_g, priv_funding_h) - - val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) - val update_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) - val update_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) - val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) - val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, channelId_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, channelId_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, channelId_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val scid_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val scid_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val scid_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val scid_ef = RealShortChannelId(BlockHeight(420000), 4, 0) + val scid_ag_private = RealShortChannelId(BlockHeight(420000), 5, 0) + val scid_gh = RealShortChannelId(BlockHeight(420000), 6, 0) + + val channelId_ag_private = randomBytes32() + + val alias_ab = ShortChannelId.generateLocalAlias() + val alias_ag_private = ShortChannelId.generateLocalAlias() + + val scids_ab = ShortIds(RealScidStatus.Final(scid_ab), alias_ab, None) + val scids_ag_private = ShortIds(RealScidStatus.Final(scid_ag_private), alias_ag_private, None) + + val chan_ab = channelAnnouncement(scid_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) + val chan_bc = channelAnnouncement(scid_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) + val chan_cd = channelAnnouncement(scid_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) + val chan_ef = channelAnnouncement(scid_ef, priv_e, priv_f, priv_funding_e, priv_funding_f) + val chan_gh = channelAnnouncement(scid_gh, priv_g, priv_h, priv_funding_g, priv_funding_h) + + val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) + val update_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, scid_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) + val update_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) + val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) + val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) + val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) + val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) override def withFixture(test: OneArgTest): Outcome = { // the network will be a --(1)--> b ---(2)--> c --(3)--> d @@ -106,13 +114,14 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // and e --(4)--> f (we are a) within(30 seconds) { // first we make sure that we correctly resolve channelId+direction to nodeId - assert(Router.getDesc(update_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, a, b)) - assert(Router.getDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) - assert(Router.getDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) - assert(Router.getDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(Router.getDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(channelId_ag_private, a, g)) - assert(Router.getDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(channelId_ag_private, a, g)) - assert(Router.getDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) + assert(ChannelDesc(update_ab, chan_ab) == ChannelDesc(chan_ab.shortChannelId, a, b)) + assert(ChannelDesc(update_bc, chan_bc) == ChannelDesc(chan_bc.shortChannelId, b, c)) + assert(ChannelDesc(update_cd, chan_cd) == ChannelDesc(chan_cd.shortChannelId, c, d)) + assert(ChannelDesc(update_ef, chan_ef) == ChannelDesc(chan_ef.shortChannelId, e, f)) + val privateChannel_ag = PrivateChannel(channelId_ag_private, scids_ag_private, a, g, None, None, ChannelMeta(1000 msat, 1000 msat)) + assert(ChannelDesc(update_ag_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, a, g)) + assert(ChannelDesc(update_ga_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, g, a)) + assert(ChannelDesc(update_gh, chan_gh) == ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router val sender = TestProbe() @@ -122,7 +131,6 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi import com.softwaremill.quicklens._ val nodeParams = Alice.nodeParams .modify(_.nodeKeyManager).setTo(testNodeKeyManager) - .modify(_.routerConf.routerBroadcastInterval).setTo(1 day) // "disable" auto rebroadcast val router = system.actorOf(Router.props(nodeParams, watcher.ref)) // we announce channels peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ab)) @@ -150,13 +158,14 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32(), channelId_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) + sender.send(router, ShortChannelIdAssigned(sender.ref, channelId_ag_private, scids_ag_private, remoteNodeId = g)) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_bc) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_cd) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ef) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_gh) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_bc) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_cd) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ef) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_gh) // and answers with valid scripts watcher.send(router, ValidateResult(chan_ab, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_b)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) watcher.send(router, ValidateResult(chan_bc, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) @@ -171,7 +180,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, ) - assert(watchedShortChannelIds === Set(channelId_ab, channelId_bc, channelId_cd, channelId_ef, channelId_gh)) + assert(watchedShortChannelIds == Set(scid_ab, scid_bc, scid_cd, scid_ef, scid_gh)) // all messages are acked peerConnection.expectMsgAllOf( GossipDecision.Accepted(chan_ab), @@ -204,7 +213,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val channels = sender.expectMsgType[Iterable[ChannelAnnouncement]] sender.send(router, GetChannelUpdates) val updates = sender.expectMsgType[Iterable[ChannelUpdate]] - nodes.size === 8 && channels.size === 5 && updates.size === 11 + nodes.size == 8 && channels.size == 5 && updates.size == 11 }, max = 10 seconds, interval = 1 second) withFixture(test.toNoArgTest(FixtureParam(nodeParams, router, watcher))) @@ -213,12 +222,15 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi } object BaseRouterSpec { - def channelAnnouncement(channelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) val node2_sig = Announcements.signChannelAnnouncement(witness, node2_priv) val funding2_sig = Announcements.signChannelAnnouncement(witness, funding2_priv) - makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) + makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } + + def channelHopFromUpdate(nodeId: PublicKey, nextNodeId: PublicKey, channelUpdate: ChannelUpdate): ChannelHop = + ChannelHop(channelUpdate.shortChannelId, nodeId, nextNodeId, ChannelRelayParams.FromAnnouncement(channelUpdate)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index f44d9936d6..2baf31a3a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -16,13 +16,14 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.{ChannelMeta, PublicChannel} import fr.acinq.eclair.router.Sync._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags import fr.acinq.eclair.wire.protocol.QueryShortChannelIdsTlv.QueryFlagType._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ -import fr.acinq.eclair.wire.protocol.{EncodedShortChannelIds, EncodingType, ReplyChannelRange} +import fr.acinq.eclair.wire.protocol.{EncodedShortChannelIds, EncodingType, LightningMessageCodecs, ReplyChannelRange} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -109,29 +110,29 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L))) // no extended info but we know the channel: we ask for the updates - assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) - assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = false) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // same checksums, newer timestamps: we don't ask anything - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), includeNodeAnnouncements = true) === 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), includeNodeAnnouncements = true) == 0) // different checksums, newer timestamps: we ask for the updates - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // different checksums, older timestamps: we don't ask anything - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) === 0) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) === 0) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), includeNodeAnnouncements = true) === 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) == 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) == 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), includeNodeAnnouncements = true) == 0) // missing channel update: we ask for it - assert(computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // unknown channel: we ask everything - assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) - assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = false) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) } - def makeShortChannelIds(height: BlockHeight, count: Int): List[ShortChannelId] = { - val output = ArrayBuffer.empty[ShortChannelId] + def makeShortChannelIds(height: BlockHeight, count: Int): List[RealShortChannelId] = { + val output = ArrayBuffer.empty[RealShortChannelId] var txIndex = 0 var outputIndex = 0 while (output.size < count) { @@ -141,7 +142,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } else { outputIndex = outputIndex + 1 } - output += ShortChannelId(height, txIndex, outputIndex) + output += RealShortChannelId(height, txIndex, outputIndex) } output.toList } @@ -152,7 +153,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { // check that chunks contain exactly the ids they were built from are are consistent i.e each chunk covers a range that immediately follows // the previous one even if there are gaps in block heights - def validate(ids: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { + def validate(ids: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { @tailrec def noOverlap(chunks: List[ShortChannelIdsChunk]): Boolean = chunks match { @@ -189,14 +190,14 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { test("split short channel ids correctly (basic tests") { - def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = ShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) + def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) // no ids to split { val ids = Nil val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -205,7 +206,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -214,7 +215,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1100) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -223,7 +224,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -233,7 +234,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000, 0), id(1000, 1), id(1000, 2), id(1000, 3), id(1000, 4), id(1000, 5)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -242,7 +243,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 6, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1006), 8, List(ids(2), ids(3))), @@ -255,7 +256,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 12, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1013), 1040 - 1013 + 1, List(ids(3), ids(4))), @@ -268,7 +269,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 105 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 2, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1002), 2, List(ids(2), ids(3))), @@ -281,7 +282,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 4 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 2, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1003), 2, List(ids(3), ids(4))) @@ -293,13 +294,13 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = makeShortChannelIds(BlockHeight(1000), 100) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } } test("split short channel ids correctly") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) val firstBlock = BlockHeight(0) val numberOfBlocks = 1000 @@ -311,7 +312,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("split short channel ids correctly (comprehensive tests)") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) for (firstBlock <- 0 to 60) { for (numberOfBlocks <- 1 to 60) { for (chunkSize <- 1 :: 2 :: 20 :: 50 :: 100 :: 1000 :: Nil) { @@ -356,6 +357,16 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } } + test("encode maximum size reply_channel_range") { + val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => RealShortChannelId(i)).toList + val timestamps = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Timestamps(i.unixsec, (i + 1).unixsec)).toList + val checksums = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Checksums(i, i + 1)).toList + val reply = ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 100, 0, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, scids), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)), Some(EncodedChecksums(checksums))) + val encoded = LightningMessageCodecs.lightningMessageCodec.encode(reply) + assert(encoded.isSuccessful) + assert(encoded.require.bytes.length <= 0xffff) + } + test("do not encode empty lists as COMPRESSED_ZLIB") { { val reply = buildReplyChannelRange(ShortChannelIdsChunk(BlockHeight(0), 42, Nil), syncComplete = true, Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_ALL)), SortedMap()) @@ -374,4 +385,5 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 42L, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, None)) } } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala new file mode 100644 index 0000000000..643e682d5c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -0,0 +1,206 @@ +package fr.acinq.eclair.router + +import akka.actor.ActorSystem +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchExternalChannelSpent, WatchExternalChannelSpentTriggered, WatchFundingDeeplyBuriedTriggered} +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.channel.{CMD_CLOSE, DATA_NORMAL} +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge +import fr.acinq.eclair.router.Router._ +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, Shutdown} +import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} +import org.scalatest.OptionValues.convertOptionToValuable +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) { + //@formatter:off + /** there is only one channel here */ + def privateChannel: PrivateChannel = router.stateData.privateChannels.values.head + def publicChannel: PublicChannel = router.stateData.channels.values.head + //@formatter:on + } + + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + + override def withFixture(test: OneArgTest): Outcome = { + val channels = init(tags = test.tags) + val rebroadcastListener = TestProbe() + val router: TestFSMRef[Router.State, Router.Data, Router] = { + // we use alice's actor system so we share the same event stream + implicit val system: ActorSystem = channels.alice.underlying.system + system.eventStream.subscribe(rebroadcastListener.ref, classOf[Router.Rebroadcast]) + TestFSMRef(new Router(channels.alice.underlyingActor.nodeParams, channels.alice.underlyingActor.blockchain, initialized = None)) + } + withFixture(test.toNoArgTest(FixtureParam(router, rebroadcastListener, channels, test.tags))) + } + + private def internalTest(f: FixtureParam): Unit = { + import f._ + + val (aliceNodeId, bobNodeId) = (channels.alice.underlyingActor.nodeParams.nodeId, channels.bob.underlyingActor.nodeParams.nodeId) + reachNormal(channels, testTags, interceptChannelUpdates = false) + + // the router learns about the local, still unannounced, channel + awaitCond(router.stateData.privateChannels.size == 1) + + // only alice's channel_update is known (NB : due to how node ids are constructed, 1 = alice and 2 = bob) + assert(privateChannel.update_1_opt.isDefined) + assert(privateChannel.update_2_opt.isEmpty) + // alice will only have a real scid if this is not a zeroconf channel + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.toOption.isEmpty == f.testTags.contains(ChannelStateTestsTags.ZeroConf)) + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.isDefined) + // alice uses her alias for her internal channel update + val aliceInitialChannelUpdate = privateChannel.update_1_opt.value + assert(aliceInitialChannelUpdate.shortChannelId == privateChannel.shortIds.localAlias) + + // alice and bob send their channel_updates using remote alias when they go to NORMAL state + val aliceChannelUpdate1 = channels.alice2bob.expectMsgType[ChannelUpdate] + val bobChannelUpdate1 = channels.bob2alice.expectMsgType[ChannelUpdate] + // alice's channel_update uses bob's alias, and vice versa + assert(aliceChannelUpdate1.shortChannelId == channels.bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + assert(bobChannelUpdate1.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + // channel_updates are handled by the peer connection and sent to the router + val peerConnection = TestProbe() + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate1) + + // router processes bob's channel_update and now knows both channel updates + awaitCond { + privateChannel.update_1_opt.contains(aliceInitialChannelUpdate) && privateChannel.update_2_opt.contains(bobChannelUpdate1) + } + + // there is nothing for the router to rebroadcast, channel is not announced + assert(router.stateData.rebroadcast == Rebroadcast(Map.empty, Map.empty, Map.empty)) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // this is a public channel + // funding tx reaches 6 blocks, announcements are exchanged + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.alice2bob.expectMsgType[AnnouncementSignatures] + channels.alice2bob.forward(channels.bob) + + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.forward(channels.alice) + + // the router learns about the announcement and channel graduates from private to public + awaitCond { + router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + } + + // router has cleaned up the scid mapping + assert(router.stateData.scid2PrivateChannels.isEmpty) + + // alice and bob won't send their channel_update directly to each other because the channel has been announced + // but we can get the update from their data + awaitCond { + channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined && + channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined + } + val aliceChannelUpdate2 = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + val bobChannelUpdate2 = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + // this time, they use the real scid + val aliceAnn = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + val bobAnn = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + assert(aliceAnn == bobAnn) + assert(aliceChannelUpdate2.shortChannelId == aliceAnn.shortChannelId) + assert(bobChannelUpdate2.shortChannelId == bobAnn.shortChannelId) + + // the router has already processed the new local channel update from alice which uses the real scid, and keeps bob's previous channel update + assert(publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate1)) + + // the router prepares to rebroadcast the channel announcement, the local update which uses the real scid, and the first node announcement + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map(aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip)), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // bob's channel_update reaches the router + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate2) + + // router processes bob's channel_update and now knows both channel updates with real scids + awaitCond { + publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate2) + } + + // router is now ready to rebroadcast both channel updates + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map( + aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip), + bobChannelUpdate2 -> Set[GossipOrigin](RemoteGossip(peerConnection.ref, bobNodeId)) + ), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().size == 2) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate2, publicChannel), GraphEdge(bobChannelUpdate2, publicChannel))) + } else { + // this is a private channel + // funding tx reaches 6 blocks, no announcements are exchanged because the channel is private + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + + // alice and bob won't send their channel_update directly to each other because they haven't changed + channels.alice2bob.expectNoMessage(100 millis) + channels.bob2alice.expectNoMessage(100 millis) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + } + // channel closes + channels.alice ! CMD_CLOSE(TestProbe().ref, scriptPubKey = None, feerates = None) + channels.alice2bob.expectMsgType[Shutdown] + channels.alice2bob.forward(channels.bob) + channels.bob2alice.expectMsgType[Shutdown] + channels.bob2alice.forward(channels.alice) + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // if the channel was public, the router asked the watcher to watch the funding tx and will be notified + val watchSpentBasic = channels.alice2blockchain.expectMsgType[WatchExternalChannelSpent] + watchSpentBasic.replyTo ! WatchExternalChannelSpentTriggered(watchSpentBasic.shortChannelId) + } + // router cleans up the channel + awaitAssert { + assert(router.stateData.nodes == Map.empty) + assert(router.stateData.channels == Map.empty) + assert(router.stateData.privateChannels == Map.empty) + assert(router.stateData.scid2PrivateChannels == Map.empty) + assert(router.stateData.graphWithBalances.graph.edgeSet().isEmpty) + // TODO: we're not currently pruning nodes without channels from the graph, but we should! + // assert(router.stateData.graphWithBalances.graph.vertexSet().isEmpty) + } + } + + test("private local channel") { f => + internalTest(f) + } + + test("private local channel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) + } + + test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + internalTest(f) + } + + test("public local channel (zeroconf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index b3dafaefe0..a62561bb47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -16,27 +16,29 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, yenKshortestPaths} +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios, yenKshortestPaths} import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.router.Router.ChannelDesc import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId} +import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ class GraphSpec extends AnyFunSuite { - val (a, b, c, d, e, f, g) = ( + val (a, b, c, d, e, f, g, h) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //c PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //d PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //e PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc"), //f - PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") //g + PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), //g + PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5") //h ) // +---- D -------+ @@ -44,7 +46,7 @@ class GraphSpec extends AnyFunSuite { // A --- B ------ C // | | // +--- E --+ - def makeTestGraph() = + private def makeTestGraph() = DirectedGraph().addEdges(Seq( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), @@ -62,10 +64,10 @@ class GraphSpec extends AnyFunSuite { .addVertex(e) assert(graph.containsVertex(a) && graph.containsVertex(e)) - assert(graph.vertexSet().size === 5) + assert(graph.vertexSet().size == 5) val otherGraph = graph.addVertex(a) // adding the same vertex twice! - assert(otherGraph.vertexSet().size === 5) + assert(otherGraph.vertexSet().size == 5) // add some edges to the graph val edgeAB = makeEdge(1L, a, b, 0 msat, 0) @@ -81,14 +83,14 @@ class GraphSpec extends AnyFunSuite { .addEdge(edgeDC) .addEdge(edgeCE) - assert(graphWithEdges.edgesOf(a).size === 2) - assert(graphWithEdges.edgesOf(b).size === 1) - assert(graphWithEdges.edgesOf(c).size === 1) - assert(graphWithEdges.edgesOf(d).size === 1) - assert(graphWithEdges.edgesOf(e).size === 0) + assert(graphWithEdges.edgesOf(a).size == 2) + assert(graphWithEdges.edgesOf(b).size == 1) + assert(graphWithEdges.edgesOf(c).size == 1) + assert(graphWithEdges.edgesOf(d).size == 1) + assert(graphWithEdges.edgesOf(e).size == 0) val withRemovedEdges = graphWithEdges.removeEdge(edgeAD.desc) - assert(withRemovedEdges.edgesOf(d).size === 1) + assert(withRemovedEdges.edgesOf(d).size == 1) } test("instantiate a graph adding edges only") { @@ -106,10 +108,10 @@ class GraphSpec extends AnyFunSuite { .addEdge(edgeCE) .addEdge(edgeBE) - assert(graph.vertexSet().size === 5) - assert(graph.edgesOf(c).size === 1) - assert(graph.getIncomingEdgesOf(c).size === 2) - assert(graph.edgeSet().size === 6) + assert(graph.vertexSet().size == 5) + assert(graph.edgesOf(c).size == 1) + assert(graph.getIncomingEdgesOf(c).size == 2) + assert(graph.edgeSet().size == 6) } test("containsEdge should return true if the graph contains that edge, false otherwise") { @@ -139,14 +141,14 @@ class GraphSpec extends AnyFunSuite { val edgeAD = makeEdge(3L, a, d, 0 msat, 0) val edgeDC = makeEdge(4L, d, c, 0 msat, 0) - assert(graph.edgeSet().size === 6) + assert(graph.edgeSet().size == 6) assert(graph.containsEdge(edgeBE.desc)) val withRemovedEdge = graph.removeEdge(edgeBE.desc) - assert(withRemovedEdge.edgeSet().size === 5) + assert(withRemovedEdge.edgeSet().size == 5) val withRemovedList = graph.removeEdges(Seq(edgeAD.desc, edgeDC.desc)) - assert(withRemovedList.edgeSet().size === 4) + assert(withRemovedList.edgeSet().size == 4) val withoutAnyIncomingEdgeInE = graph.removeEdges(Seq(edgeBE.desc, edgeCE.desc)) assert(withoutAnyIncomingEdgeInE.containsVertex(e)) @@ -161,19 +163,19 @@ class GraphSpec extends AnyFunSuite { )) val edgesAB = graph.getEdgesBetween(a, b) - assert(edgesAB.size === 1) // there should be an edge a --> b - assert(edgesAB.head.desc.a === a) - assert(edgesAB.head.desc.b === b) + assert(edgesAB.size == 1) // there should be an edge a --> b + assert(edgesAB.head.desc.a == a) + assert(edgesAB.head.desc.b == b) val bIncoming = graph.getIncomingEdgesOf(b) - assert(bIncoming.size === 1) - assert(bIncoming.exists(_.desc.a === a)) // there should be an edge a --> b - assert(bIncoming.exists(_.desc.b === b)) + assert(bIncoming.size == 1) + assert(bIncoming.exists(_.desc.a == a)) // there should be an edge a --> b + assert(bIncoming.exists(_.desc.b == b)) val bOutgoing = graph.edgesOf(b) - assert(bOutgoing.size === 1) - assert(bOutgoing.exists(_.desc.a === b)) - assert(bOutgoing.exists(_.desc.b === c)) + assert(bOutgoing.size == 1) + assert(bOutgoing.exists(_.desc.a == b)) + assert(bOutgoing.exists(_.desc.b == c)) } test("there can be multiple edges between the same vertices") { @@ -192,13 +194,13 @@ class GraphSpec extends AnyFunSuite { val mutatedGraph2 = mutatedGraph.addEdge(edgeForTheSameChannel) assert(mutatedGraph2.edgesOf(a).size == 3) // A --> B , A --> B , A --> D - assert(mutatedGraph2.getEdgesBetween(a, b).size === 2) - assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.update.feeBaseMsat === 30.msat) + assert(mutatedGraph2.getEdgesBetween(a, b).size == 2) + assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.params.relayFees.feeBase == 30.msat) } test("remove a vertex with incoming edges and check those edges are removed too") { val graph = makeTestGraph() - assert(graph.vertexSet().size === 5) + assert(graph.vertexSet().size == 5) assert(graph.containsVertex(e)) assert(graph.containsEdge(descFromNodes(5, c, e))) assert(graph.containsEdge(descFromNodes(6, b, e))) @@ -206,7 +208,7 @@ class GraphSpec extends AnyFunSuite { // E has 2 incoming edges val withoutE = graph.removeVertex(e) - assert(withoutE.vertexSet().size === 4) + assert(withoutE.vertexSet().size == 4) assert(!withoutE.containsVertex(e)) assert(!withoutE.containsEdge(descFromNodes(5, c, e))) assert(!withoutE.containsEdge(descFromNodes(6, b, e))) @@ -219,26 +221,26 @@ class GraphSpec extends AnyFunSuite { val edgeDC = makeEdge(4L, d, c, 0 msat, 0, capacity = 800 sat, balance_opt = Some(50000 msat)) val graph = DirectedGraph(Seq(edgeAB, edgeAD, edgeBC, edgeDC)) - assert(graph.edgesOf(a).toSet === Set(edgeAB, edgeAD)) - assert(graph.getIncomingEdgesOf(a) === Nil) - assert(graph.edgesOf(c) === Nil) - assert(graph.getIncomingEdgesOf(c).toSet === Set(edgeBC, edgeDC)) + assert(graph.edgesOf(a).toSet == Set(edgeAB, edgeAD)) + assert(graph.getIncomingEdgesOf(a) == Nil) + assert(graph.edgesOf(c) == Nil) + assert(graph.getIncomingEdgesOf(c).toSet == Set(edgeBC, edgeDC)) val edgeAB1 = edgeAB.copy(balance_opt = Some(200000 msat)) val edgeBC1 = edgeBC.copy(balance_opt = Some(150000 msat)) val graph1 = graph.addEdge(edgeAB1).addEdge(edgeBC1) - assert(graph1.edgesOf(a).toSet === Set(edgeAB1, edgeAD)) - assert(graph1.getIncomingEdgesOf(a) === Nil) - assert(graph1.edgesOf(c) === Nil) - assert(graph1.getIncomingEdgesOf(c).toSet === Set(edgeBC1, edgeDC)) + assert(graph1.edgesOf(a).toSet == Set(edgeAB1, edgeAD)) + assert(graph1.getIncomingEdgesOf(a) == Nil) + assert(graph1.edgesOf(c) == Nil) + assert(graph1.getIncomingEdgesOf(c).toSet == Set(edgeBC1, edgeDC)) } def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = makeEdge(shortChannelId, a, b, 0 msat, 0).desc def edgeFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): GraphEdge = makeEdge(shortChannelId, a, b, 0 msat, 0) - test("amount with fees larger than channel capacity") { + test("amount with fees larger than channel capacity for C->D") { /* The channel C -> D is just large enough for the payment to go through but when adding the channel fee it becomes too big. A --> B --> C <-> D @@ -257,8 +259,117 @@ class GraphSpec extends AnyFunSuite { val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, - Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20))), + Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } + + test("fees less along C->D->E than C->E") { + /* + The channel going through C -> D -> E (fee = 1001 + 1 = 1002) is considered shorter than going + through C -> E (fee = 1003) because the fees are less. + A --> B --> C <-> D + \ / + \ / + E + */ + val edgeAB = makeEdge(1L, a, b, 10001 msat, 0, capacity = 200000 sat) + val edgeBC = makeEdge(2L, b, c, 10000 msat, 0, capacity = 200000 sat) + val edgeCD = makeEdge(3L, c, d, 10001 msat, 0, capacity = 200000 sat) + val edgeDC = makeEdge(4L, d, c, 10 msat, 0, capacity = 200000 sat) + val edgeCE = makeEdge(5L, c, e, 10003 msat, 0, capacity = 200000 sat) + val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) + val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) + + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + Set.empty, Set.empty, Set.empty, 2, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + + assert(paths.length == 2) + assert(paths(0).path == Seq(edgeAB, edgeBC, edgeCD, edgeDE)) + assert(paths(1).path == Seq(edgeAB, edgeBC, edgeCE)) + } + + test("Path C->D->E selected because amount too great for C->E capacity") { + /* + The channel C -> D -> E is a valid path, but C -> E is not. + A --> B --> C <-> D + \ / + \ / + E + */ + val edgeAB = makeEdge(1L, a, b, 10001 msat, 0, capacity = 200000 sat) + val edgeBC = makeEdge(2L, b, c, 10000 msat, 0, capacity = 200000 sat) + val edgeCD = makeEdge(3L, c, d, 20001 msat, 0, capacity = 200000 sat) + val edgeDC = makeEdge(4L, d, c, 10 msat, 0, capacity = 200000 sat) + val edgeCE = makeEdge(5L, c, e, 10003 msat, 0, capacity = 10000 sat) + val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) + val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) + + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + Set.empty, Set.empty, Set.empty, 2, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + + // Even though paths to find is 2, we only find 1 because that is all the valid paths that there are. + assert(paths.length == 1) + assert(paths.head.path == Seq(edgeAB, edgeBC, edgeCD, edgeDE)) + } + + /** + * Find all the shortest paths using the example described in + * https://en.wikipedia.org/wiki/Yen's_algorithm#Example + */ + test("all shortest paths are found") { + // There will be 3 shortest paths. + // Edge capacities are set to be the same so that only feeBase will affect the RichWeight. + // C --> D --> F + // \ ^ / | \ + // \ | / | \ + // \ | / | \ + // E --> G --> H + val edgeCD = makeEdge(1L, c, d, 3 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeDF = makeEdge(2L, d, f, 4 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeCE = makeEdge(3L, c, e, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeED = makeEdge(4L, e, d, 1 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeEF = makeEdge(5L, e, f, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeFG = makeEdge(6L, f, g, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeFH = makeEdge(7L, f, h, 1 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeEG = makeEdge(8L, e, g, 3 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) + + val paths = yenKshortestPaths(graph, c, h, 10000000 msat, + Set.empty, Set.empty, Set.empty, 3, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + assert(paths.length == 3) + assert(paths(0).path == Seq(edgeCE, edgeEF, edgeFH)) + assert(paths(1).path == Seq(edgeCE, edgeEG, edgeGH)) + assert(paths(2).path == Seq(edgeCD, edgeDF, edgeFH)) + } + + test("RoutingHeuristics.normalize") { + // value inside the range + assert(Graph.RoutingHeuristics.normalize(value = 10, min = 0, max = 100) === (10.0 / 100.0) +- 0.001) + assert(Graph.RoutingHeuristics.normalize(value = 20, min = 10, max = 200) === (10.0 / 190.0) +- 0.001) + assert(Graph.RoutingHeuristics.normalize(value = -11, min = -100, max = -10) === (89.0 / 90.0) +- 0.001) + + // value on the bounds + assert(Graph.RoutingHeuristics.normalize(value = 0, min = 0, max = 100) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 10, min = 10, max = 200) > 0) + assert(Graph.RoutingHeuristics.normalize(value = -100, min = -100, max = -10) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 9.1, min = 10, max = 200) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 100, min = 0, max = 100) < 1) + assert(Graph.RoutingHeuristics.normalize(value = 200, min = 10, max = 200) < 1) + + // value outside the range + assert(Graph.RoutingHeuristics.normalize(value = 105.2, min = 0, max = 100) < 1) + + // Should throw exception if min > max + assertThrows[IllegalArgumentException]( + Graph.RoutingHeuristics.normalize(value = 9, min = 10, max = 1) + ) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index a8f990e8a5..338b3f3ea6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -17,10 +17,11 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{HeuristicsConstants, RichWeight, WeightRatios} @@ -28,7 +29,8 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, ShortChannelIdSpec, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ @@ -56,7 +58,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("check fee against max pct properly") { @@ -77,7 +79,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("calculate the shortest path (correct fees)") { @@ -121,9 +123,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route :: Nil) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, BlockHeight(0), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) - assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil) - assert(weightedPath.length === 3) - assert(weightedPath.amount === expectedCost) + assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) + assert(weightedPath.length == 3) + assert(weightedPath.amount == expectedCost) // update channel 5 so that it can route the final amount (10000) but not the amount + fees (10002) val graph1 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10001 msat))) @@ -131,7 +133,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val graph3 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, balance_opt = Some(10001 msat))) for (g <- Seq(graph1, graph2, graph3)) { val Success(route1 :: Nil) = findRoute(g, a, d, amount, maxFee = 10 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } } @@ -145,7 +147,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 2 :: 5 :: Nil) + assert(route2Ids(route) == 2 :: 5 :: Nil) } test("calculate simple route (add and remove edges") { @@ -157,11 +159,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2 === Failure(RouteNotFound)) + assert(route2 == Failure(RouteNotFound)) } test("calculate the shortest path (hardcoded nodes)") { @@ -180,7 +182,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 4 :: 3 :: Nil) + assert(route2Ids(route) == 4 :: 3 :: Nil) } test("calculate the shortest path (select direct channel)") { @@ -199,8 +201,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: route2 :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 4 :: Nil) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 4 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: Nil) } test("find a route using channels with htlMaximumMsat close to the payment amount") { @@ -219,7 +221,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("find a route using channels with htlMinimumMsat close to the payment amount") { @@ -238,7 +240,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("if there are multiple channels between the same node, select the cheapest") { @@ -257,7 +259,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 6 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 6 :: 3 :: Nil) } test("if there are multiple channels between the same node, select one that has enough balance") { @@ -276,7 +278,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("calculate longer but cheaper route") { @@ -289,7 +291,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("no local channels") { @@ -299,7 +301,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("route not found") { @@ -310,7 +312,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("route not found (source OR target node not connected)") { @@ -319,8 +321,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, d, 0 msat, 0) )).addVertex(a).addVertex(e) - assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) - assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (amount too high OR too low)") { @@ -342,8 +344,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edgesHi) val g1 = DirectedGraph(edgesLo) - assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) - assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (balance too low)") { @@ -372,7 +374,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) )) - Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound))) + Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) } test("route to self") { @@ -383,7 +385,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(CannotRouteToSelf)) + assert(route == Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { @@ -395,7 +397,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: Nil) + assert(route2Ids(route) == 1 :: Nil) } test("directed graph") { @@ -408,10 +410,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2 === Failure(RouteNotFound)) + assert(route2 == Failure(RouteNotFound)) } test("calculate route and return metadata") { @@ -427,41 +429,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) val edges = Seq( - GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), uab, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), uba, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ubc, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ucb, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ucd, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), udc, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ude, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ued, DEFAULT_CAPACITY, None) + GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), ChannelRelayParams.FromAnnouncement(uab), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), ChannelRelayParams.FromAnnouncement(uba), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ChannelRelayParams.FromAnnouncement(ubc), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ChannelRelayParams.FromAnnouncement(ucb), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ChannelRelayParams.FromAnnouncement(ucd), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), ChannelRelayParams.FromAnnouncement(udc), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ChannelRelayParams.FromAnnouncement(ude), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ChannelRelayParams.FromAnnouncement(ued), DEFAULT_CAPACITY, None) ) val g = DirectedGraph(edges) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route.hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil) - } - - test("convert extra hops to assisted channels") { - val a = randomKey().publicKey - val b = randomKey().publicKey - val c = randomKey().publicKey - val d = randomKey().publicKey - val e = randomKey().publicKey - - val extraHop1 = ExtraHop(a, ShortChannelId(1), 12.sat.toMilliSatoshi, 10000, CltvExpiryDelta(12)) - val extraHop2 = ExtraHop(b, ShortChannelId(2), 200.sat.toMilliSatoshi, 0, CltvExpiryDelta(22)) - val extraHop3 = ExtraHop(c, ShortChannelId(3), 150.sat.toMilliSatoshi, 0, CltvExpiryDelta(32)) - val extraHop4 = ExtraHop(d, ShortChannelId(4), 50.sat.toMilliSatoshi, 0, CltvExpiryDelta(42)) - val extraHops = extraHop1 :: extraHop2 :: extraHop3 :: extraHop4 :: Nil - - val amount = 90000 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW - val assistedChannels = toAssistedChannels(extraHops, e, amount.toMilliSatoshi) - - assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(extraHop4, e, 100050.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(extraHop3, d, 100200.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop2.shortChannelId) === AssistedChannel(extraHop2, c, 100400.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop1.shortChannelId) === AssistedChannel(extraHop1, b, 101416.sat.toMilliSatoshi)) + assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } test("blacklist routes") { @@ -473,7 +453,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route1 === Failure(RouteNotFound)) + assert(route1 == Failure(RouteNotFound)) // verify that we left the graph untouched assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) @@ -482,7 +462,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // make sure we can find a route if without the blacklist val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) } test("route to a destination that is not in the graph (with assisted routes)") { @@ -493,12 +473,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) // now we add the missing edge to reach the destination val extraGraphEdges = Set(makeEdge(4L, d, e, 5 msat, 5)) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) } test("route from a source that is not in the graph (with assisted routes)") { @@ -508,12 +488,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) // now we add the missing starting edge val extraGraphEdges = Set(makeEdge(1L, a, b, 5 msat, 5)) val Success(route1 :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } test("verify that extra hops takes precedence over known channels") { @@ -525,13 +505,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route1.hops(1).lastUpdate.feeBaseMsat === 10.msat) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) + assert(route1.hops(1).params.relayFees.feeBase == 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route2.hops(1).lastUpdate.feeBaseMsat === 5.msat) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2.hops(1).params.relayFees.feeBase == 5.msat) } test("compute ignored channels") { @@ -565,7 +545,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val publicChannels = channels.map { case (shortChannelId, announcement) => - val update = edges.find(_.desc.shortChannelId == shortChannelId).get.update + val ChannelRelayParams.FromAnnouncement(update) = edges.find(_.desc.shortChannelId == shortChannelId).get.params val (update_1_opt, update_2_opt) = if (update.channelFlags.isNode1) (Some(update), None) else (None, Some(update)) val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt, update_2_opt, None) (shortChannelId, pc) @@ -587,10 +567,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) - assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 18)) - assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 19)) - assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 20)) - assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) + assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) + assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 20)) + assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("ignore cheaper route when it has more than 20 hops") { @@ -606,7 +586,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(expensiveShortEdge :: edges) val Success(route :: Nil) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 0 :: 1 :: 99 :: 48 :: Nil) + assert(route2Ids(route) == 0 :: 1 :: 99 :: 48 :: Nil) } test("ignore cheaper route when it has more than the requested CLTV") { @@ -621,7 +601,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil) + assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) } test("ignore cheaper route when it grows longer than the requested size") { @@ -636,7 +616,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 6 :: Nil) + assert(route2Ids(route) == 1 :: 6 :: Nil) } test("ignore loops") { @@ -649,7 +629,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 4 :: 5 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 4 :: 5 :: Nil) } test("ensure the route calculation terminates correctly when selecting 0-fees edges") { @@ -665,7 +645,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 3 :: 5 :: Nil) + assert(route2Ids(route) == 1 :: 3 :: 5 :: Nil) } // +---+ +---+ +---+ @@ -698,19 +678,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(fourShortestPaths.size === 4) - assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F - assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F - assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F - assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) === 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F + assert(fourShortestPaths.size == 4) + assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F + assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F + assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) == 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F + assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) == 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F // Update balance D -> A to evict the last path (balance too low) val g2 = g1.addEdge(makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat))) val threeShortestPaths = Graph.yenKshortestPaths(g2, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(threeShortestPaths.size === 3) - assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F - assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F - assert(hops2Ids(threeShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F + assert(threeShortestPaths.size == 3) + assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F + assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F + assert(hops2Ids(threeShortestPaths(2).path.map(graphEdgeToHop)) == 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F } test("find the k shortest path (wikipedia example)") { @@ -737,12 +717,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(twoShortestPaths.size === 2) + assert(twoShortestPaths.size == 2) val shortest = twoShortestPaths(0) - assert(hops2Ids(shortest.path.map(graphEdgeToHop)) === 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H + assert(hops2Ids(shortest.path.map(graphEdgeToHop)) == 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H val secondShortest = twoShortestPaths(1) - assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) === 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H + assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) == 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H } test("terminate looking for k-shortest path if there are no more alternative paths than k, must not consider routes going back on their steps") { @@ -767,9 +747,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // we ask for 3 shortest paths but only 2 can be found val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(foundPaths.size === 2) - assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F - assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) === 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F + assert(foundPaths.size == 2) + assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) == 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F + assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) == 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F } test("select a random route below the requested fee") { @@ -778,7 +758,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { .modify(_.boundaries.maxFeeProportional).setTo(0) .modify(_.randomize).setTo(true) val strictFee = strictFeeParams.getMaxFee(DEFAULT_AMOUNT_MSAT) - assert(strictFee === 7.msat) + assert(strictFee == 7.msat) // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 10000103 !! @@ -795,12 +775,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { for (_ <- 0 to 10) { val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 2, routes) + assert(routes.length == 2, routes) val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) val totalFees = weightedPath.amount - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees) - assert(totalFees === 5.msat || totalFees === 6.msat) - assert(weightedPath.length === 3) + assert(totalFees == 5.msat || totalFees == 6.msat) + assert(weightedPath.length == 3) } } @@ -822,7 +802,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(routeFeeOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeFeeOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) val Success(routeCltvOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, @@ -831,7 +811,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0, hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) + assert(route2Nodes(routeCltvOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, @@ -840,19 +820,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 1, hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeCapacityOptimized) === (a, e) :: (e, c) :: (c, d) :: Nil) + assert(route2Nodes(routeCapacityOptimized) == (a, e) :: (e, c) :: (c, d) :: Nil) } test("prefer going through an older channel if fees and CLTV are the same") { val currentBlockHeight = BlockHeight(554000) val g = DirectedGraph(List( - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x1").toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x4").toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong - 3000}x0x2").toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong - 3000}x0x3").toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x5").toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x6").toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x1").success.value.toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x4").success.value.toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x2").success.value.toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x3").success.value.toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x5").success.value.toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x6").success.value.toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) )) val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( @@ -863,7 +843,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = currentBlockHeight) - assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) } test("prefer a route with a smaller total CLTV if fees and score are the same") { @@ -884,7 +864,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) } test("avoid a route that breaks off the max CLTV") { @@ -907,35 +887,35 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeScoreOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) } test("cost function is monotonic") { // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. val updates = SortedMap( - ShortChannelId("565643x1216x0") -> PublicChannel( - ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), + RealShortChannelId(BlockHeight(565643), 1216, 0) -> PublicChannel( + ann = makeChannel(ShortChannelId.fromCoordinates("565643x1216x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None ), - ShortChannelId("542280x2156x0") -> PublicChannel( - ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), + RealShortChannelId(BlockHeight(542280), 2156, 0) -> PublicChannel( + ann = makeChannel(ShortChannelId.fromCoordinates("542280x2156x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None ), - ShortChannelId("565779x2711x0") -> PublicChannel( - ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), + RealShortChannelId(BlockHeight(565779), 2711, 0) -> PublicChannel( + ann = makeChannel(ShortChannelId.fromCoordinates("565779x2711x0").success.value.toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), meta_opt = None ) ) @@ -986,7 +966,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 4, routes) + assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } @@ -999,7 +979,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { // We set min-part-amount to a value that excludes channels 1 and 4. val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1013,9 +993,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 25000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 1, routes) + assert(routes.length == 1, routes) checkRouteAmounts(routes, amount, 0 msat) - assert(route2Ids(routes.head) === 1L :: Nil) + assert(route2Ids(routes.head) == 1L :: Nil) } test("calculate multipart route to neighbor (many channels, some balance unknown)") { @@ -1029,7 +1009,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 65000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 4, routes) + assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } @@ -1048,7 +1028,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 3, routes) + assert(routes.length == 3, routes) assert(routes.forall(_.length == 1), routes) checkIgnoredChannels(routes, 2L) checkRouteAmounts(routes, amount, 0 msat) @@ -1094,7 +1074,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute // should ignore this information. - val pendingHtlcs = Seq(Route(10000 msat, ChannelHop(a, b, edge_ab_1.update) :: Nil), Route(5000 msat, ChannelHop(a, b, edge_ab_2.update) :: Nil)) + val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) @@ -1143,7 +1123,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 30000 msat val maxFeeTooLow = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(maxFeeTooLow === Failure(RouteNotFound)) + assert(maxFeeTooLow == Failure(RouteNotFound)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 20 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length <= 2), routes) @@ -1161,11 +1141,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } } @@ -1178,7 +1158,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val result = findMultiPartRoute(g, a, b, 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } test("cannot find multipart route to neighbor (restricted htlc_minimum_msat)") { @@ -1190,11 +1170,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } } @@ -1218,14 +1198,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Update A - B with unknown balance, capacity should be used instead. val g1 = g.addEdge(edge_ab.copy(capacity = 15 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Randomize routes. @@ -1236,19 +1216,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // Update balance A - B to be too low. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(2000 msat))) val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low. val g1 = g.addEdge(edge_ab.copy(capacity = 5 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1276,7 +1256,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // But we don't want to split such tiny amounts. val (amount, maxFee) = (2000 msat, 150 msat) val failure = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1290,8 +1270,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes.length === 1, "payment shouldn't be split when we have one path with enough capacity") - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L))) + assert(routes.length == 1, "payment shouldn't be split when we have one path with enough capacity") + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L))) } test("calculate multipart route to remote node (single local channel)") { @@ -1315,7 +1295,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Randomize routes. @@ -1327,25 +1307,25 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g1 = g.addEdge(edge_ab.copy(capacity = 500 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Update balance A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(400000 msat))) val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(capacity = 400 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1380,23 +1360,23 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(100L, 101L))) + assert(routes2Ids(routes) == Set(Seq(100L, 101L))) } { val amount = 15_000_000 msat val maxFee = 10_000 msat // this fee is too low to go through the preferred route val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { val amount = 5_000_000 msat val maxFee = 10_000 msat // this fee is enough to go through the preferred route, but the cheaper ones can handle it val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 5) + assert(routes.length == 5) routes.foreach(route => { - assert(route.length === 2) + assert(route.length == 2) assert(route.amount <= 1_200_000.msat) assert(!route.hops.flatMap(h => Seq(h.nodeId, h.nextNodeId)).contains(c)) }) @@ -1431,7 +1411,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ignoredChannels = Set(ChannelDesc(ShortChannelId(2L), b, c)) val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, ignoredEdges = ignoredChannels, ignoredVertices = ignoredNodes, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(8L), Seq(9L, 10L))) + assert(routes2Ids(routes) == Set(Seq(8L), Seq(9L, 10L))) } test("calculate multipart route to remote node (restricted htlc_minimum_msat and htlc_maximum_msat)") { @@ -1460,7 +1440,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val maxFeeTooLow = 3 msat val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } test("calculate multipart route to remote node (complex graph)") { @@ -1497,7 +1477,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val (amount, maxFee) = (25000 msat, 50 msat) val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { val (amount, maxFee) = (40000 msat, 100 msat) @@ -1512,7 +1492,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val (amount, maxFee) = (40000 msat, 50 msat) val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1544,7 +1524,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val maxFeeTooLow = 40 msat val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } test("calculate multipart route to remote node (pending htlcs)") { @@ -1566,7 +1546,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), )) - val pendingHtlcs = Seq(Route(5000 msat, ChannelHop(a, b, edge_ab.update) :: ChannelHop(b, e, edge_be.update) :: Nil)) + val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil)) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 2), routes) checkRouteAmounts(routes, amount, maxFee) @@ -1607,7 +1587,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { case Success(routes) => checkRouteAmounts(routes, amount, maxFee) - case Failure(ex) => assert(ex === RouteNotFound) + case Failure(ex) => assert(ex == RouteNotFound) } } } @@ -1633,8 +1613,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes - assert(route2Ids(route1) === 1 :: 5 :: Nil) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 5 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) } test("reversed loop trap") { @@ -1658,8 +1638,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes - assert(route2Ids(route1) === 5 :: 1 :: Nil) - assert(route2Ids(route2) === 4 :: 3 :: 2 :: 1 :: Nil) + assert(route2Ids(route1) == 5 :: 1 :: Nil) + assert(route2Ids(route2) == 4 :: 3 :: 2 :: 1 :: Nil) } test("k-shortest paths must be distinct") { @@ -1725,7 +1705,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) - val fees = routes.map(_.fee) + val fees = routes.map(_.fee(false)) assert(fees.forall(_ == fees.head)) } @@ -1734,7 +1714,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(1L, a, b, 1000 msat, 7000), )) - assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, a, b, 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) } @@ -1762,19 +1742,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) } { // small base hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(100 msat, 0)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } { // large proportional hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 200)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 1 :: Nil) + assert(route2Ids(route) == 0 :: 1 :: Nil) } } @@ -1792,16 +1772,32 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, b, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), )) - val hc = HeuristicsConstants( - lockedFundsRisk = 0.0, - failureCost = RelayFees(1000 msat, 500), - hopCost = RelayFees(0 msat, 0), - ) - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) - assert(routes.distinct.length == 1) - val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(1000 msat, 500), + hopCost = RelayFees(0 msat, 0), + useLogProbability = false, + ) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) + } + + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(10000 msat, 1000), + hopCost = RelayFees(0 msat, 0), + useLogProbability = true, + ) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) + } } test("no path that can get our funds stuck for too long") { @@ -1822,11 +1818,91 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), + useLogProbability = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) + } + + test("edge too small to relay payment is ignored") { + // A ===> B ===> C <--- D + val g = DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100), + makeEdge(2L, b, c, 100 msat, 100), + makeEdge(3L, d, c, 100 msat, 100, capacity = 1000 sat), + )) + + val hc = HeuristicsConstants( + lockedFundsRisk = 1e-7, + failureCost = RelayFees(0 msat, 0), + hopCost = RelayFees(0 msat, 0), + useLogProbability = true, + ) + val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) == 1 :: 2 :: Nil) + } + + test("use direct channel when available") { + // A ===> B ===> C + // \___________/ + val recentChannelId = ShortChannelId.fromCoordinates("399990x1x2").success.value.toLong + val g = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 1, capacity = 100_000_000 sat), + makeEdge(2L, b, c, 1 msat, 1, capacity = 100_000_000 sat), + makeEdge(recentChannelId, a, c, 1000 msat, 100), + )) + + val wr = WeightRatios( + baseFactor = 0, + cltvDeltaFactor = 0, + ageFactor = 0.5, + capacityFactor = 0.5, + hopCost = RelayFees(500 msat, 200), + ) + val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) == recentChannelId :: Nil) + } + + test("trampoline relay with direct channel to target") { + val amount = 100_000_000 msat + val g = DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))) + + { + val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) + assert(findMultiPartRoute(g, a, b, amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + } + { + val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(101_000 msat, 0.0, 6, CltvExpiryDelta(576))) + assert(findMultiPartRoute(g, a, b, amount, 101_000 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)).isSuccess) + } + } + + test("small local edge with liquidity is better than big remote edge") { + // A == B == C -- D + // \_________/ + val g = DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat, balance_opt = Some(10000000 msat)), + makeEdge(2L, b, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + makeEdge(3L, a, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100 sat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, d, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + )) + + val wr = WeightRatios( + baseFactor = 0, + cltvDeltaFactor = 0, + ageFactor = 0, + capacityFactor = 1, + hopCost = RelayFees(500 msat, 200), + ) + val Success(routes) = findRoute(g, a, d, 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr), includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) + val route :: Nil = routes + assert(route2Ids(route) == 3 :: 4 :: Nil) } } @@ -1851,7 +1927,7 @@ object RouteCalculationSpec { def makeChannel(shortChannelId: Long, nodeIdA: PublicKey, nodeIdB: PublicKey): ChannelAnnouncement = { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeIdA, nodeIdB)) (nodeIdA, nodeIdB) else (nodeIdB, nodeIdA) - ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) + ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, RealShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) } def makeEdge(shortChannelId: Long, @@ -1865,7 +1941,7 @@ object RouteCalculationSpec { capacity: Satoshi = DEFAULT_CAPACITY, balance_opt: Option[MilliSatoshi] = None): GraphEdge = { val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) - GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), update, capacity, balance_opt) + GraphEdge(ChannelDesc(RealShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) } def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = @@ -1882,25 +1958,25 @@ object RouteCalculationSpec { htlcMaximumMsat = maxHtlc ) - def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.lastUpdate.shortChannelId.toLong) + def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.shortChannelId.toLong) def route2Ids(route: Route): Seq[Long] = hops2Ids(route.hops) def routes2Ids(routes: Seq[Route]): Set[Seq[Long]] = routes.map(route2Ids).toSet - def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate, 0 sat, None)) + def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 0 sat, None)) def route2Nodes(route: Route): Seq[(PublicKey, PublicKey)] = route.hops.map(hop => (hop.nodeId, hop.nextNodeId)) def checkIgnoredChannels(routes: Seq[Route], shortChannelIds: Long*): Unit = { shortChannelIds.foreach(shortChannelId => routes.foreach(route => { - assert(route.hops.forall(_.lastUpdate.shortChannelId.toLong != shortChannelId), route) + assert(route.hops.forall(_.shortChannelId.toLong != shortChannelId), route) })) } def checkRouteAmounts(routes: Seq[Route], totalAmount: MilliSatoshi, maxFee: MilliSatoshi): Unit = { assert(routes.map(_.amount).sum == totalAmount, routes) - assert(routes.map(_.fee).sum <= maxFee, routes) + assert(routes.map(_.fee(false)).sum <= maxFee, routes) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 146a347a44..a69736bdc9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -19,21 +19,25 @@ package fr.acinq.eclair.router import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge +import fr.acinq.eclair.router.Graph.RoutingHeuristics import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} import scodec.bits._ import scala.concurrent.duration._ @@ -53,17 +57,17 @@ class RouterSpec extends BaseRouterSpec { { // valid channel announcement, no stashing - val chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) + val chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = TimestampSecond.now() + 1) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) peerConnection.expectNoMessage(100 millis) // we don't immediately acknowledge the announcement (back pressure) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ac) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ac) watcher.send(router, ValidateResult(chan_ac, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_ac)) peerConnection.expectMsg(GossipDecision.Accepted(chan_ac)) assert(peerConnection.sender() == router) - assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId === chan_ac.shortChannelId) + assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId == chan_ac.shortChannelId) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ac)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ac)) peerConnection.expectMsg(GossipDecision.Accepted(update_ac)) @@ -83,12 +87,12 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, stashing while validating channel announcement val priv_u = randomKey() val priv_funding_u = randomKey() - val chan_uc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) + val chan_uc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)) peerConnection.expectNoMessage(200 millis) // we don't immediately acknowledge the announcement (back pressure) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_uc) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_uc) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_uc)) peerConnection.expectMsg(TransportHandler.ReadAck(update_uc)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_u)) @@ -97,7 +101,7 @@ class RouterSpec extends BaseRouterSpec { peerConnection.expectMsg(TransportHandler.ReadAck(chan_uc)) peerConnection.expectMsg(GossipDecision.Accepted(chan_uc)) assert(peerConnection.sender() == router) - assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId === chan_uc.shortChannelId) + assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId == chan_uc.shortChannelId) peerConnection.expectMsg(GossipDecision.Accepted(update_uc)) peerConnection.expectMsg(GossipDecision.Accepted(node_u)) eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_uc, 2000000 sat, None, None) :: Nil)) @@ -128,7 +132,7 @@ class RouterSpec extends BaseRouterSpec { { // invalid signatures val invalid_node_b = node_b.copy(timestamp = node_b.timestamp + 10) - val invalid_chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) + val invalid_chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) val invalid_update_ab = update_ab.copy(cltvExpiryDelta = CltvExpiryDelta(21), timestamp = update_ab.timestamp + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_node_b)) peerConnection.expectMsg(TransportHandler.ReadAck(invalid_node_b)) @@ -148,7 +152,7 @@ class RouterSpec extends BaseRouterSpec { // pruned channel val priv_v = randomKey() val priv_funding_v = randomKey() - val chan_vc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) + val chan_vc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) nodeParams.db.network.addToPruned(chan_vc.shortChannelId :: Nil) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_vc)) peerConnection.expectMsg(TransportHandler.ReadAck(chan_vc)) @@ -173,7 +177,7 @@ class RouterSpec extends BaseRouterSpec { // unknown channel val priv_y = randomKey() val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, ShortChannelId(4646464), CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ay)) peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay)) @@ -189,11 +193,11 @@ class RouterSpec extends BaseRouterSpec { // invalid announcement + reject stashed val priv_y = randomKey() val priv_funding_y = randomKey() // a-y will have an invalid script - val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) + val chan_ay = channelAnnouncement(RealShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ay) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ay) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ay)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_y)) @@ -211,9 +215,9 @@ class RouterSpec extends BaseRouterSpec { { // validation failure val priv_x = randomKey() - val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) + val chan_ax = channelAnnouncement(RealShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ax)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ax) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ax) watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException("funding tx not found")))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_ax)) peerConnection.expectMsg(GossipDecision.ValidationFailure(chan_ax)) @@ -226,9 +230,9 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx not confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_az) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = false)))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_az)) peerConnection.expectMsg(GossipDecision.ChannelClosing(chan_az)) @@ -241,9 +245,9 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_az) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true)))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_az)) peerConnection.expectMsg(GossipDecision.ChannelClosed(chan_az)) @@ -261,20 +265,20 @@ class RouterSpec extends BaseRouterSpec { val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent]) - router ! WatchExternalChannelSpentTriggered(channelId_ab) - eventListener.expectMsg(ChannelLost(channelId_ab)) + router ! WatchExternalChannelSpentTriggered(scid_ab) + eventListener.expectMsg(ChannelLost(scid_ab)) // a doesn't have any channels, b still has one with c eventListener.expectMsg(NodeLost(a)) eventListener.expectNoMessage(200 milliseconds) - router ! WatchExternalChannelSpentTriggered(channelId_cd) - eventListener.expectMsg(ChannelLost(channelId_cd)) + router ! WatchExternalChannelSpentTriggered(scid_cd) + eventListener.expectMsg(ChannelLost(scid_cd)) // d doesn't have any channels, c still has one with b eventListener.expectMsg(NodeLost(d)) eventListener.expectNoMessage(200 milliseconds) - router ! WatchExternalChannelSpentTriggered(channelId_bc) - eventListener.expectMsg(ChannelLost(channelId_bc)) + router ! WatchExternalChannelSpentTriggered(scid_bc) + eventListener.expectMsg(ChannelLost(scid_bc)) // now b and c do not have any channels eventListener.expectMsgAllOf(NodeLost(b), NodeLost(c)) eventListener.expectNoMessage(200 milliseconds) @@ -284,7 +288,7 @@ class RouterSpec extends BaseRouterSpec { test("handle bad signature for ChannelAnnouncement") { fixture => import fixture._ val peerConnection = TestProbe() - val channelId_ac = ShortChannelId(BlockHeight(420000), 105, 0) + val channelId_ac = RealShortChannelId(BlockHeight(420000), 105, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) val buggy_chan_ac = chan_ac.copy(nodeSignature1 = chan_ac.nodeSignature2) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, buggy_chan_ac)) @@ -339,13 +343,13 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId == d) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res1 = sender.expectMsgType[RouteResponse] - assert(res1.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) - assert(res1.routes.head.hops.last.nextNodeId === h) + assert(res1.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) + assert(res1.routes.head.hops.last.nextNodeId == h) } test("route found (with extra routing info)") { fixture => @@ -357,10 +361,10 @@ class RouterSpec extends BaseRouterSpec { val extraHop_cx = ExtraHop(c, ShortChannelId(1), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_xy = ExtraHop(x, ShortChannelId(2), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) - sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil), routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, extraEdges = Bolt11Invoice.toExtraEdges(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil, z), routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil) - assert(res.routes.head.hops.last.nextNodeId === z) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) + assert(res.routes.head.hops.last.nextNodeId == z) } test("route not found (channel disabled)") { fixture => @@ -369,10 +373,10 @@ class RouterSpec extends BaseRouterSpec { val peerConnection = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId == d) - val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) + val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) peerConnection.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -384,11 +388,11 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) - assert(res.routes.head.hops.last.nextNodeId === h) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) + assert(res.routes.head.hops.last.nextNodeId == h) - val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) + val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, alias_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -409,7 +413,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments1)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -425,7 +429,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] - val bc = ChannelDesc(channelId_bc, b, c) + val bc = ChannelDesc(scid_bc, b, c) // let's exclude channel b->c sender.send(router, ExcludeChannel(bc)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -447,25 +451,7 @@ class RouterSpec extends BaseRouterSpec { assert(state.channels.size == 5) assert(state.nodes.size == 8) assert(state.channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 10) - state.channels.foreach(c => assert(c.capacity === publicChannelCapacity)) - } - - test("send local channels") { fixture => - import fixture._ - // We need a channel update from our private remote peer, otherwise we can't create invoice routing information. - val peerConnection = TestProbe() - peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, g, update_ga_private)) - val sender = TestProbe() - sender.send(router, GetLocalChannels) - val localChannels = sender.expectMsgType[Seq[LocalChannel]] - assert(localChannels.size === 2) - assert(localChannels.map(_.remoteNodeId).toSet === Set(b, g)) - assert(localChannels.exists(_.isPrivate)) // a ---> g - assert(localChannels.exists(!_.isPrivate)) // a ---> b - assert(localChannels.flatMap(_.toExtraHop).toSet === Set( - ExtraHop(b, channelId_ab, update_ba.feeBaseMsat, update_ba.feeProportionalMillionths, update_ba.cltvExpiryDelta), - ExtraHop(g, channelId_ag_private, update_ga_private.feeBaseMsat, update_ga_private.feeProportionalMillionths, update_ga_private.cltvExpiryDelta) - )) + state.channels.foreach(c => assert(c.capacity == publicChannelCapacity)) } test("given a pre-defined nodes route add the proper channel updates") { fixture => @@ -477,23 +463,23 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) === preComputedRoute.nodes.dropRight(1)) - assert(response.routes.head.hops.map(_.nextNodeId) === preComputedRoute.nodes.drop(1)) - assert(response.routes.head.hops.map(_.lastUpdate) === Seq(update_ab, update_bc, update_cd)) + assert(response.routes.head.hops.map(_.nodeId) == preComputedRoute.nodes.dropRight(1)) + assert(response.routes.head.hops.map(_.nextNodeId) == preComputedRoute.nodes.drop(1)) + assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined channels route add the proper channel updates") { fixture => import fixture._ val sender = TestProbe() - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_bc, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) === Seq(a, b, c)) - assert(response.routes.head.hops.map(_.nextNodeId) === Seq(b, c, d)) - assert(response.routes.head.hops.map(_.lastUpdate) === Seq(update_ab, update_bc, update_cd)) + assert(response.routes.head.hops.map(_.nodeId) == Seq(a, b, c)) + assert(response.routes.head.hops.map(_.nextNodeId) == Seq(b, c, d)) + assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined private channels route add the proper channel updates") { fixture => @@ -501,24 +487,36 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { - val preComputedRoute = PredefinedChannelRoute(g, Seq(channelId_ag_private)) + // using the channel alias + val preComputedRoute = PredefinedChannelRoute(g, Seq(alias_ag_private)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + val response = sender.expectMsgType[RouteResponse] + assert(response.routes.length == 1) + val route = response.routes.head + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.hops.head.nodeId == a) + assert(route.hops.head.nextNodeId == g) + } + { + // using the real scid + val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.lastUpdate) === Seq(update_ag_private)) - assert(route.hops.head.nodeId === a) - assert(route.hops.head.nextNodeId === g) + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.hops.head.nodeId == a) + assert(route.hops.head.nextNodeId == g) } { - val preComputedRoute = PredefinedChannelRoute(h, Seq(channelId_ag_private, channelId_gh)) + val preComputedRoute = PredefinedChannelRoute(h, Seq(scid_ag_private, scid_gh)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, g)) - assert(route.hops.map(_.nextNodeId) === Seq(g, h)) - assert(route.hops.map(_.lastUpdate) === Seq(update_ag_private, update_gh)) + assert(route.hops.map(_.nodeId) == Seq(a, g)) + assert(route.hops.map(_.nextNodeId) == Seq(g, h)) + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) } } @@ -528,34 +526,34 @@ class RouterSpec extends BaseRouterSpec { val targetNodeId = randomKey().publicKey { - val invoiceRoutingHint = ExtraHop(b, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) - val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ab, invoiceRoutingHint.shortChannelId)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + val invoiceRoutingHint = Invoice.BasicEdge(b, targetNodeId, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ab, invoiceRoutingHint.shortChannelId)) + val amount = 10_000.msat + // the amount affects the way we estimate the channel capacity of the hinted channel + assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) + sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, b)) - assert(route.hops.map(_.nextNodeId) === Seq(b, targetNodeId)) - assert(route.hops.head.lastUpdate === update_ab) - assert(route.hops.last.lastUpdate.shortChannelId === invoiceRoutingHint.shortChannelId) - assert(route.hops.last.lastUpdate.feeBaseMsat === invoiceRoutingHint.feeBase) - assert(route.hops.last.lastUpdate.feeProportionalMillionths === invoiceRoutingHint.feeProportionalMillionths) - assert(route.hops.last.lastUpdate.cltvExpiryDelta === invoiceRoutingHint.cltvExpiryDelta) + assert(route.hops.map(_.nodeId) == Seq(a, b)) + assert(route.hops.map(_.nextNodeId) == Seq(b, targetNodeId)) + assert(route.hops.head.params == ChannelRelayParams.FromAnnouncement(update_ab)) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } { - val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) - val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ag_private, channelId_gh, invoiceRoutingHint.shortChannelId)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + val invoiceRoutingHint = Invoice.BasicEdge(h, targetNodeId, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ag_private, scid_gh, invoiceRoutingHint.shortChannelId)) + val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 + // the amount affects the way we estimate the channel capacity of the hinted channel + assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) + sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, g, h)) - assert(route.hops.map(_.nextNodeId) === Seq(g, h, targetNodeId)) - assert(route.hops.map(_.lastUpdate).dropRight(1) === Seq(update_ag_private, update_gh)) - assert(route.hops.last.lastUpdate.shortChannelId === invoiceRoutingHint.shortChannelId) - assert(route.hops.last.lastUpdate.feeBaseMsat === invoiceRoutingHint.feeBase) - assert(route.hops.last.lastUpdate.feeProportionalMillionths === invoiceRoutingHint.feeProportionalMillionths) - assert(route.hops.last.lastUpdate.cltvExpiryDelta === invoiceRoutingHint.cltvExpiryDelta) + assert(route.hops.map(_.nodeId) == Seq(a, g, h)) + assert(route.hops.map(_.nextNodeId) == Seq(g, h, targetNodeId)) + assert(route.hops.map(_.params).dropRight(1) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } } @@ -564,22 +562,22 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_bc)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_bc, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_bc, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, ShortChannelId(1105), channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, ShortChannelId(1105), scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } @@ -588,7 +586,7 @@ class RouterSpec extends BaseRouterSpec { test("ask for channels that we marked as stale for which we receive a new update") { fixture => import fixture._ val blockHeight = BlockHeight(400000) - 2020 - val channelId = ShortChannelId(blockHeight, 5, 0) + val channelId = RealShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) val oldTimestamp = TimestampSecond.now() - 14.days - 1.day val staleUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = oldTimestamp) @@ -622,24 +620,24 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(channel_ab.meta_opt === None) + assert(channel_ab.meta_opt == None) { // When the local channel comes back online, it will send a LocalChannelUpdate to the router. val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val g = sender.expectMsgType[Data].graphWithBalances.graph + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { @@ -651,51 +649,51 @@ class RouterSpec extends BaseRouterSpec { // Then we update the balance without changing the contents of the channel update; the graph should still be updated. val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat)) val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val g = sender.expectMsgType[Data].graphWithBalances.graph + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { // When HTLCs are relayed through the channel, balance changes are sent to the router. val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) - sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ab, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, null, scids_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val g = sender.expectMsgType[Data].graphWithBalances.graph + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { // Private channels should also update the graph when HTLCs are relayed through them. val balances = Set(33000000 msat, 5000000 msat) val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false) - sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ag_private, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scids_ab, commitments)) sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] val channel_ag = data.privateChannels(channelId_ag_private) - assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) === balances) + assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) == balances) // And the graph should be updated too. - val edge_ag = data.graph.getEdge(ChannelDesc(channelId_ag_private, a, g)).get + val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(alias_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) - assert(edge_ag.balance_opt === Some(33000000 msat)) + assert(edge_ag.balance_opt == Some(33000000 msat)) } } @@ -719,7 +717,7 @@ class RouterSpec extends BaseRouterSpec { false case RoutingStateStreamingUpToDate => true } - assert(nodes.size === 8 && channels.size === 5 && updates.size === 10) // public channels only + assert(nodes.size == 8 && channels.size == 5 && updates.size == 10) // public channels only // just making sure that we have been subscribed to network events, otherwise there is a possible race condition awaitCond({ @@ -728,7 +726,7 @@ class RouterSpec extends BaseRouterSpec { }, max = 30 seconds) // new announcements - val update_ab_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum, timestamp = update_ab.timestamp + 1) + val update_ab_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum, timestamp = update_ab.timestamp + 1) val peerConnection = TestProbe() router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab_2) sender.fishForMessage() { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 7d7d800e5e..987c5f22d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -19,9 +19,10 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, Props} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler @@ -35,7 +36,7 @@ import fr.acinq.eclair.wire.protocol._ import org.scalatest.ParallelTestExecution import org.scalatest.funsuite.AnyFunSuiteLike -import scala.collection.immutable.{TreeMap, SortedSet} +import scala.collection.immutable.{SortedSet, TreeMap} import scala.collection.mutable import scala.concurrent.duration._ @@ -45,10 +46,10 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle // this map will store private keys so that we can sign new announcements at will val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty - val fakeRoutingInfo: TreeMap[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec + val fakeRoutingInfo: TreeMap[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec .shortChannelIds .take(60) - .foldLeft(TreeMap.empty[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { + .foldLeft(TreeMap.empty[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(pub2priv)(shortChannelId)) } @@ -127,7 +128,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle SyncResult(rcrs, queries, channels, updates, nodes) } - def countUpdates(channels: Map[ShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { + def countUpdates(channels: Map[RealShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { case (count, pc) => count + pc.update_1_opt.map(_ => 1).getOrElse(0) + pc.update_2_opt.map(_ => 1).getOrElse(0) } @@ -140,9 +141,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val extendedQueryFlags_opt = None // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) - awaitCond(alice.stateData.nodes === bob.stateData.nodes) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) + awaitCond(alice.stateData.nodes == bob.stateData.nodes) // add some channels and updates to bob and resync fakeRoutingInfo.take(10).values.foreach { @@ -153,18 +154,18 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = 10 * 2) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = 10 * 2) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add some updates to bob and resync fakeRoutingInfo.take(10).values.foreach { case (pc, _, _) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10 * 2) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10 * 2, nodes = 10 * 2) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10 * 2) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10 * 2, nodes = 10 * 2) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { @@ -175,9 +176,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && countUpdates(bob.stateData.channels) === 2 * fakeRoutingInfo.size, max = 60 seconds) - assert(BasicSyncResult(ranges = 3, queries = 13, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == fakeRoutingInfo.size && countUpdates(bob.stateData.channels) == 2 * fakeRoutingInfo.size, max = 60 seconds) + assert(BasicSyncResult(ranges = 3, queries = 13, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) } def syncWithExtendedQueries(requestNodeAnnouncements: Boolean): Unit = { @@ -189,8 +190,8 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val extendedQueryFlags_opt = Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add some channels and updates to bob and resync fakeRoutingInfo.take(10).values.foreach { @@ -201,19 +202,19 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) - if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes == bob.stateData.nodes) // add some updates to bob and resync fakeRoutingInfo.take(10).values.foreach { case (pc, _, _) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10 * 2) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 0, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10 * 2) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 0, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { @@ -224,9 +225,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && countUpdates(bob.stateData.channels) === 2 * fakeRoutingInfo.size, max = 60 seconds) - assert(BasicSyncResult(ranges = 3, queries = 11, channels = fakeRoutingInfo.size - 10, updates = 2 * (fakeRoutingInfo.size - 10), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 10) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == fakeRoutingInfo.size && countUpdates(bob.stateData.channels) == 2 * fakeRoutingInfo.size, max = 60 seconds) + assert(BasicSyncResult(ranges = 3, queries = 11, channels = fakeRoutingInfo.size - 10, updates = 2 * (fakeRoutingInfo.size - 10), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 10) else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) // bump random channel_updates def touchUpdate(shortChannelId: Int, side: Boolean) = { @@ -236,9 +237,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val bumpedUpdates = (List(0, 3, 7).map(touchUpdate(_, side = true)) ++ List(1, 3, 9).map(touchUpdate(_, side = false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - assert(BasicSyncResult(ranges = 3, queries = 1, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 5 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) - if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + assert(BasicSyncResult(ranges = 3, queries = 1, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 5 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes == bob.stateData.nodes) } test("sync with extended channel queries (don't request node announcements)") { @@ -263,12 +264,12 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = true, None)) val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) // ask router to send another channel range query sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = false, None)) sender.expectNoMessage(100 millis) // it's a duplicate and should be ignored - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, fakeRoutingInfo.take(params.routerConf.channelQueryChunkSize).keys.toList), None, None) @@ -276,7 +277,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, block1)) // router should ask for our first block of ids - assert(peerConnection.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, TlvStream.empty)) + assert(peerConnection.expectMsgType[QueryShortChannelIds] == QueryShortChannelIds(chainHash, block1.shortChannelIds, TlvStream.empty)) // router should think that it is missing 100 channels, in one request val Some(sync) = router.stateData.sync.get(remoteNodeId) assert(sync.remainingQueries.isEmpty) // the request was sent already @@ -286,7 +287,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = true, None)) sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) } test("reject unsolicited sync") { @@ -310,7 +311,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), TlvStream.empty) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42))), TlvStream.empty) val nodeIdA = randomKey().publicKey val nodeIdB = randomKey().publicKey @@ -331,15 +332,15 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle object RoutingSyncSpec { - lazy val shortChannelIds: SortedSet[ShortChannelId] = (for { + lazy val shortChannelIds: SortedSet[RealShortChannelId] = (for { block <- 400000 to 420000 txindex <- 0 to 5 outputIndex <- 0 to 1 - } yield ShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + } yield RealShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[RealShortChannelId])(_ + _) val unused: PrivateKey = randomKey() - def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: ShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { + def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: RealShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { val timestamp = TimestampSecond.now() val (priv1, priv2) = { val (priv_a, priv_b) = (randomKey(), randomKey()) @@ -352,7 +353,7 @@ object RoutingSyncSpec { val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2) val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) - val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features) + val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), Features.empty) val publicChannel = PublicChannel(channelAnn_12, ByteVector32.Zeroes, Satoshi(0), Some(channelUpdate_12), Some(channelUpdate_21), None) (publicChannel, nodeAnnouncement_1, nodeAnnouncement_2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala new file mode 100644 index 0000000000..3c1817a3ad --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala @@ -0,0 +1,44 @@ +package fr.acinq.eclair.testutils + +import org.scalatest.Reporter +import org.scalatest.events.{Event, RunCompleted, SuiteCompleted, TestSucceeded} + +/** + * A scalatest reporter that displays the slowest tests and suites. Useful to detect pain points and speed up the + * overall build. + * + * To activate, edit the project's pom.xml and set: + * {{{ + * scalatest-maven-plugin + * ... + * + * false + * fr.acinq.eclair.testutils.DurationBenchmarkReporter + * ... + * + * }}} + * + * Then run the tests as usual. + * + * Note that test parallelization needs to be disabled to have accurate measurement of durations, which will considerably + * slow down the build time. + */ +class DurationBenchmarkReporter() extends Reporter { + + val suites = collection.mutable.Map.empty[String, Long] + val tests = collection.mutable.Map.empty[String, Long] + + override def apply(event: Event): Unit = event match { + case e: TestSucceeded => + e.duration.map(d => tests.addOne(e.testName, d)) + case e: SuiteCompleted => + e.duration.map(d => suites.addOne(e.suiteName, d)) + case _: RunCompleted => + println("top 20 slowest tests:") + tests.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } + println("top 20 slowest suites:") + suites.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } + case _ => + () + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala new file mode 100644 index 0000000000..fe140a43c9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.testutils + +import org.scalatest.concurrent.Eventually +import org.scalatest.funsuite.FixtureAnyFunSuite +import org.scalatest.{Assertions, Outcome, TestData} + +/** + * This is an opinionated base class for tests that need a fixture. It is a very thin layer on top of Scalatest, which + * follows the recommended way of using fixtures, and hides boiler plate code. + * + * Usage: + * {{{ + * class MyTestSpec extends FixtureSpec { + * + * type FixtureParam = MyFixtureClass + * + * override def createFixture(testData: TestData): MyFixtureClass = { + * // will be run for each test + * // build the fixture here + * // customize with testData.tags if appropriate + * ... + * } + * + * override def cleanupFixture(fixture: MyFixtureClass): Unit = { + * fixture.cleanup() // it is a good practice to define a cleanup method in your fixture case class + * } + * + * test("my first test") { f => + * import f._ + * // write your test here + * ... + * } + * } + * }}} + * + * Recommendations: + * - on test parallelization: + * - do not use [[org.scalatest.ParallelTestExecution]], parallelize on test suites instead + * - keep the execution time of each suite under one minute + * - on actor testing: + * - use a normal [[akka.actor.ActorSystem]] and real actors, use [[akka.testkit.TestProbe]] to communicate with them + * - do not use [[akka.testkit.TestKit]] class + * - do not use [[akka.testkit.TestActorRef]] + * + * References: + * - https://hseeberger.github.io/2017/09/13/2017-09-13-how-to-use-akka-testkit/ + * - https://markuseliasson.se/article/scalatest-best-practices/ + */ +abstract class FixtureSpec extends FixtureAnyFunSuite + with Assertions + with Eventually { + + def createFixture(testData: TestData): FixtureParam + + def cleanupFixture(fixture: FixtureParam): Unit + + override final def withFixture(test: OneArgTest): Outcome = { + val fixture = createFixture(test) + try { + withFixture(test.toNoArgTest(fixture)) + } finally { + cleanupFixture(fixture) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala index a89ee1d21f..a35cfb2b1f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.tor -import java.net.InetSocketAddress +import fr.acinq.eclair.wire.protocol.NodeAddress +import java.net.InetSocketAddress import org.scalatest.funsuite.AnyFunSuite /** @@ -30,27 +31,27 @@ class Socks5ConnectionSpec extends AnyFunSuite { val proxyAddress = new InetSocketAddress(9050) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("1.2.3.4", 9735), + address = NodeAddress.fromParts("1.2.3.4", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("1.2.3.4", 9735), + address = NodeAddress.fromParts("1.2.3.4", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = false, useForTor = true, useForWatchdogs = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = false, useForWatchdogs = true)).isEmpty) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index 9b342051e5..995c529b11 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -54,7 +54,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -70,7 +69,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -86,31 +84,15 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n""")) - protocolHandler ! ByteString( - "250 OK\r\n" - ) - - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) - protocolHandler ! ByteString( - "250-ServiceID=z4zif3fy7fe7bpg3\r\n" + - "250-PrivateKey=RSA1024:private-key\r\n" + - "250 OK\r\n" - ) - protocolHandler ! GetOnionAddress - expectMsg(Some(Tor2("z4zif3fy7fe7bpg3", 9999))) + awaitCond(promiseOnionAddress.isCompleted) - val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === Tor2("z4zif3fy7fe7bpg3", 9999)) - - assert(readString(PkFilePath) === "RSA1024:private-key") + assertThrows[TorException](Await.result(promiseOnionAddress.future, Duration.Inf)) } test("happy path v3") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v3"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -142,26 +124,24 @@ class TorProtocolHandlerSpec extends TestKitBaseClass expectMsg(Some(Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) + assert(address == Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) - assert(readString(PkFilePath) === "ED25519-V3:private-key") + assert(readString(PkFilePath) == "ED25519-V3:private-key") } test("v2/v3 compatibility check against tor version") { - assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6")) - assert(!OnionServiceVersion.isCompatible(V3, "0.3.3.5")) - assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6-devel")) - assert(OnionServiceVersion.isCompatible(V3, "0.4")) - assert(!OnionServiceVersion.isCompatible(V3, "0.2")) - assert(OnionServiceVersion.isCompatible(V3, "0.5.1.2.3.4")) - + assert(isCompatible("0.3.3.6")) + assert(!isCompatible("0.3.3.5")) + assert(isCompatible("0.3.3.6-devel")) + assert(isCompatible("0.4")) + assert(!isCompatible("0.2")) + assert(isCompatible("0.5.1.2.3.4")) } test("authentication method errors") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -173,13 +153,13 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'")) + } == TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'")) } test("invalid server hash") { @@ -188,7 +168,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, fr.acinq.eclair.randomBytes32().toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -200,7 +179,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -211,7 +190,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("unexpected server hash")) + } == TorException("unexpected server hash")) } @@ -221,7 +200,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -233,7 +211,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -249,7 +227,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value.")) + } == TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value.")) } test("ADD_ONION failure") { @@ -258,7 +236,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -270,7 +247,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -284,7 +261,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) protocolHandler ! ByteString( "513 Invalid argument\r\n" ) @@ -295,7 +272,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("server returned error: 513 Invalid argument")) + } == TorException("server returned error: 513 Invalid argument")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index de93b261c0..6d5a6f8ae6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, randomBytes32} @@ -31,19 +31,19 @@ class CommitmentSpecSpec extends AnyFunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) - assert(spec1 === spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) + assert(spec1 == spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) - assert(spec2 === spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) + assert(spec2 == spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, Nil, ful1 :: Nil) - assert(spec3 === spec2.copy(htlcs = Set(OutgoingHtlc(add2)), toRemote = 2000000 msat)) + assert(spec3 == spec2.copy(htlcs = Set(OutgoingHtlc(add2)), toRemote = 2000000 msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, Nil, fail1 :: Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toLocal = 3000000 msat)) + assert(spec4 == spec3.copy(htlcs = Set(), toLocal = 3000000 msat)) } test("add, fulfill and fail htlcs from the receiver side") { @@ -53,26 +53,26 @@ class CommitmentSpecSpec extends AnyFunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) - assert(spec1 === spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) + assert(spec1 == spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) - assert(spec2 === spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) + assert(spec2 == spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, ful1 :: Nil, Nil) - assert(spec3 === spec2.copy(htlcs = Set(IncomingHtlc(add2)), toLocal = (2000 * 1000) msat)) + assert(spec3 == spec2.copy(htlcs = Set(IncomingHtlc(add2)), toLocal = (2000 * 1000) msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, fail1 :: Nil, Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) + assert(spec4 == spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) } test("compute htlc tx feerate based on commitment format") { val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(2500 sat), toLocal = (5000 * 1000) msat, toRemote = (2500 * 1000) msat) - assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === FeeratePerKw(0 sat)) + assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) == FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(0 sat)) } def createHtlc(amount: MilliSatoshi): UpdateAddHtlc = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 67e7fe4242..5761570969 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFeatures import fr.acinq.eclair.channel.Helpers.Funding @@ -123,8 +124,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) - val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, isFunder = true, Local.payment_basepoint, Remote.payment_basepoint) - assert(obscured_tx_number === (0x2bb038521914L ^ 42L)) + val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) + assert(obscured_tx_number == (0x2bb038521914L ^ 42L)) logger.info(s"local_payment_basepoint: ${Local.payment_basepoint}") logger.info(s"remote_payment_basepoint: ${Remote.payment_basepoint}") @@ -184,7 +185,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"local_feerate_per_kw: ${spec.commitTxFeerate}") val outputs = Transactions.makeCommitTxOutputs( - localIsFunder = true, + localIsInitiator = true, localDustLimit = Local.dustLimit, localRevocationPubkey = Local.revocation_pubkey, toLocalDelay = Local.toSelfDelay, @@ -203,7 +204,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { commitTxNumber = Local.commitTxNumber, localPaymentBasePoint = Local.payment_basepoint, remotePaymentBasePoint = Remote.payment_basepoint, - localIsFunder = true, + localIsInitiator = true, outputs = outputs) val local_sig = Transactions.sign(tx, Local.funding_privkey, TxOwner.Local, commitmentFormat) logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") @@ -226,7 +227,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { } }) - assert(Transactions.getCommitTxNumber(commitTx.tx, isFunder = true, Local.payment_basepoint, Remote.payment_basepoint) === Local.commitTxNumber) + assert(Transactions.getCommitTxNumber(commitTx.tx, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) == Local.commitTxNumber) Transaction.correctlySpends(commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"output commit_tx: ${commitTx.tx}") @@ -275,7 +276,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val check = (0 to 4).flatMap(i => tests(name).get(s"htlc_success_tx (htlc #$i)").toSeq ++ tests(name).get(s"htlc_timeout_tx (htlc #$i)").toSeq ).toSet.map((tx: String) => Transaction.read(tx)) - assert(htlcTxs.map(_.tx).toSet === check) + assert(htlcTxs.map(_.tx).toSet == check) } test("simple commitment tx with no HTLCs") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 130153a1fa..b124808a6c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} -import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256} +import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.SigHash._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding @@ -71,8 +72,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { parentTxId2 -> 4, parentTxId3 -> 5, ) - assert(expected === Scripts.csvTimeouts(tx)) - assert(BlockHeight(10) === Scripts.cltvTimeout(tx)) + assert(expected == Scripts.csvTimeouts(tx)) + assert(BlockHeight(10) == Scripts.cltvTimeout(tx)) } test("encode/decode sequence and lockTime (one example)") { @@ -105,7 +106,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { ) val spec = CommitmentSpec(htlcs, FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat) - assert(fee === 5340000.msat) + assert(fee == 5340000.msat) } test("check pre-computed transaction weights") { @@ -168,7 +169,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -183,7 +184,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -199,7 +200,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(anchorAmount, pubKeyScript) :: Nil, lockTime = 0) val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx, localFundingPriv.publicKey, BlockHeight(1105)) assert(claimAnchorOutputTx.tx.txOut.isEmpty) - assert(claimAnchorOutputTx.confirmBefore === BlockHeight(1105)) + assert(claimAnchorOutputTx.confirmBefore == BlockHeight(1105)) // we will always add at least one input and one output to be able to set our desired feerate // we use dummy signatures to compute the weight val p2wpkhWitness = ScriptWitness(Seq(Scripts.der(PlaceHolderSig), PlaceHolderPubKey.value)) @@ -208,7 +209,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { txOut = Seq(TxOut(1500 sat, Script.pay2wpkh(randomKey().publicKey))) )) val weight = Transaction.weight(addSigs(claimAnchorOutputTxWithFees, PlaceHolderSig).tx) - assert(weight === 717) + assert(weight == 717) assert(weight >= claimAnchorOutputMinWeight) } } @@ -221,31 +222,31 @@ class TransactionsSpec extends AnyFunSuite with Logging { { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFundeeBelowDust.toLocal - commitFee) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) + assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFundeeBelowDust.toLocal - commitFee) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi === toLocalFunderBelowDust.toRemote) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) + assert(outputs.head.output.amount.toMilliSatoshi == toLocalFunderBelowDust.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFunderBelowDust.toLocal) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) + assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFunderBelowDust.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi === toLocalFundeeBelowDust.toRemote - commitFee) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) + assert(outputs.head.output.amount.toMilliSatoshi == toLocalFundeeBelowDust.toRemote - commitFee) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) assert(outputs.isEmpty) } } @@ -280,18 +281,18 @@ class TransactionsSpec extends AnyFunSuite with Logging { toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val commitTxNumber = 0x404142434445L val commitTx = { - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } { - assert(getCommitTxNumber(commitTx.tx, isFunder = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) + assert(getCommitTxNumber(commitTx.tx, isInitiator = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) val hash = Crypto.sha256(localPaymentPriv.publicKey.value ++ remotePaymentPriv.publicKey.value) val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) @@ -299,15 +300,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { } val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(DefaultCommitmentFormat), outputs, DefaultCommitmentFormat) - assert(htlcTxs.length === 4) + assert(htlcTxs.length == 4) val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(confirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) + assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 2)) + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 2)) assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 3)) + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 3)) { // either party spends local->remote htlc output with htlc timeout tx @@ -326,7 +327,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(OutputNotFound)) + assert(htlcDelayed1 == Left(OutputNotFound)) } { // remote spends local->remote htlc1/htlc3 output directly in case of success @@ -356,7 +357,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(AmountBelowDustLimit)) + assert(htlcDelayed1 == Left(AmountBelowDustLimit)) } { // local spends main delayed output @@ -394,7 +395,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends offered HTLC output with revocation key @@ -416,7 +417,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends received HTLC output with revocation key @@ -439,44 +440,44 @@ class TransactionsSpec extends AnyFunSuite with Logging { val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.isEmpty) } } @@ -520,37 +521,37 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(htlcTxs.length === 5) + assert(htlcTxs.length == 5) val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(confirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) + assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3)) + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4)) + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) - val zeroFeeOutputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, zeroFeeOutputs) + val zeroFeeOutputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, zeroFeeOutputs) val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat), zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(zeroFeeHtlcTxs.length === 7) + assert(zeroFeeHtlcTxs.length == 7) val zeroFeeConfirmationTargets = zeroFeeHtlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(zeroFeeConfirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) + assert(zeroFeeConfirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } - zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee === 0.sat)) - zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee === 0.sat)) + zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee == 0.sat)) + zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee == 0.sat)) assert(zeroFeeHtlcTimeoutTxs.size == 3) // htlc1, htlc3 and htlc7 - assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3, 7)) + assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) assert(zeroFeeHtlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 - assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4, 8)) + assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) (commitTx, outputs, htlcTimeoutTxs, htlcSuccessTxs) } @@ -565,7 +566,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote cannot spend main output with default commitment format val Left(failure) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(failure === OutputNotFound) + assert(failure == OutputNotFound) } { // remote spends main delayed output @@ -621,7 +622,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(OutputNotFound)) + assert(htlcDelayed1 == Left(OutputNotFound)) } { // local spends offered htlc with HTLC-success tx @@ -653,7 +654,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // local can't claim delayed output of htlc4 success tx because it is below the dust limit val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayed1 === Left(AmountBelowDustLimit)) + assert(claimHtlcDelayed1 == Left(AmountBelowDustLimit)) } { // remote spends local->remote htlc outputs directly in case of success @@ -672,7 +673,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends remote->local htlc output directly in case of timeout @@ -694,7 +695,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends all htlc txs aggregated in a single tx @@ -702,13 +703,13 @@ class TransactionsSpec extends AnyFunSuite with Logging { val txOut = htlcTimeoutTxs.flatMap(_.tx.txOut) ++ htlcSuccessTxs.flatMap(_.tx.txOut) val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) val claimHtlcDelayedPenaltyTxs = makeClaimHtlcDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTxs.size === 5) + assert(claimHtlcDelayedPenaltyTxs.size == 5) val skipped = claimHtlcDelayedPenaltyTxs.collect { case Left(reason) => reason } - assert(skipped.size === 2) - assert(skipped.toSet === Set(AmountBelowDustLimit)) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) val claimed = claimHtlcDelayedPenaltyTxs.collect { case Right(tx) => tx } - assert(claimed.size === 3) - assert(claimed.map(_.input.outPoint).toSet.size === 3) + assert(claimed.size == 3) + assert(claimed.map(_.input.outPoint).toSet.size == 3) } { // remote spends offered htlc output with revocation key @@ -775,8 +776,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) @@ -794,10 +795,10 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // htlc3 and htlc4 are completely identical, their relative order can't be enforced. - assert(htlcTxs.length === 5) + assert(htlcTxs.length == 5) htlcTxs.foreach(tx => assert(tx.isInstanceOf[HtlcTimeoutTx])) val htlcIds = htlcTxs.sortBy(_.input.outPoint.index).map(_.htlcId) - assert(htlcIds === Seq(1, 2, 3, 4, 5) || htlcIds === Seq(1, 2, 4, 3, 5)) + assert(htlcIds == Seq(1, 2, 3, 4, 5) || htlcIds == Seq(1, 2, 4, 3, 5)) assert(htlcOut2.publicKeyScript.toHex < htlcOut3.publicKeyScript.toHex) assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc2))).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) @@ -814,51 +815,51 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Different amounts, both outputs untrimmed, local is funder: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 2) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) + assert(closingTx.tx.txOut.length == 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 149_000.sat) // funder pays the fee + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 149_000.sat) // funder pays the fee val toRemoteIndex = (toLocal.index + 1) % 2 - assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount === 250_000.sat) + assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount == 250_000.sat) } { // Same amounts, both outputs untrimmed, local is fundee: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = false, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 2) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) + assert(closingTx.tx.txOut.length == 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 150_000.sat) + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 150_000.sat) val toRemoteIndex = (toLocal.index + 1) % 2 assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount < 150_000.sat) } { // Their output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = false, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 1) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) + assert(closingTx.tx.txOut.length == 1) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 150_000.sat) - assert(toLocal.index === 0) + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 150_000.sat) + assert(toLocal.index == 0) } { // Our output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 1) - assert(closingTx.toLocalOutput === None) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) + assert(closingTx.tx.txOut.length == 1) + assert(closingTx.toLocalOutput == None) } { // Both outputs are trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 10_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.isEmpty) - assert(closingTx.toLocalOutput === None) + assert(closingTx.toLocalOutput == None) } } @@ -899,11 +900,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { TestVector(name, CommitmentSpec(htlcs, FeeratePerKw(feerate_per_kw.toLong.sat), MilliSatoshi(to_local_msat.toLong), MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) }).toSeq - assert(tests.size === 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests + assert(tests.size == 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests tests.foreach(test => { logger.info(s"running BOLT 3 test: '${test.name}'") val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat) - assert(fee === test.expectedFee) + assert(fee == test.expectedFee) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala index d7fcaefc78..592dc1d54a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala @@ -44,7 +44,7 @@ class CommandCodecsSpec extends AnyFunSuite { msg => val encoded = CommandCodecs.cmdCodec.encode(msg).require val decoded = CommandCodecs.cmdCodec.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } @@ -57,14 +57,14 @@ class CommandCodecsSpec extends AnyFunSuite { (("id" | int64) :: ("r" | bytes32) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFulfillCodec.decode(legacyCmdFulfillCodec.encode(42 :: data32 :: true :: HNil).require).require === + assert(CommandCodecs.cmdFulfillCodec.decode(legacyCmdFulfillCodec.encode(42 :: data32 :: true :: HNil).require).require == DecodeResult(CMD_FULFILL_HTLC(42, data32, commit = false, None), BitVector.empty)) val legacyCmdFailCodec = (("id" | int64) :: ("reason" | either(bool, varsizebinarydata, failureMessageCodec)) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFailCodec.decode(legacyCmdFailCodec.encode(42 :: Left(data123) :: true :: HNil).require).require === + assert(CommandCodecs.cmdFailCodec.decode(legacyCmdFailCodec.encode(42 :: Left(data123) :: true :: HNil).require).require == DecodeResult(CMD_FAIL_HTLC(42, Left(data123), commit = false, None), BitVector.empty)) val legacyCmdFailMalformedCodec = @@ -72,7 +72,7 @@ class CommandCodecsSpec extends AnyFunSuite { ("onionHash" | bytes32) :: ("failureCode" | uint16) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFailMalformedCodec.decode(legacyCmdFailMalformedCodec.encode(42 :: data32 :: 456 :: true :: HNil).require).require === + assert(CommandCodecs.cmdFailMalformedCodec.decode(legacyCmdFailMalformedCodec.encode(42 :: data32 :: 456 :: true :: HNil).require).require == DecodeResult(CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), BitVector.empty)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 7ae0a93c94..c50a3fb7bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -16,12 +16,12 @@ package fr.acinq.eclair.wire.internal.channel -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} -import fr.acinq.eclair._ +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.json.JsonSerializers @@ -29,11 +29,15 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, TxOwner} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs._ -import fr.acinq.eclair.wire.protocol.UpdateAddHtlc +import fr.acinq.eclair.wire.protocol.{CommonCodecs, UpdateAddHtlc} +import fr.acinq.eclair.{RealShortChannelId, _} import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite +import scodec.DecodeResult import scodec.bits._ +import scodec.codecs.byte +import java.io.{File, FileWriter} import java.util.UUID import scala.io.Source @@ -45,7 +49,7 @@ class ChannelCodecsSpec extends AnyFunSuite { test("nonreg for der/bin64 signatures") { val bin = ByteVector.fromValidHex(Source.fromInputStream(getClass.getResourceAsStream("/normal_data_htlcs.bin")).mkString) - val c = ChannelCodecs.stateDataCodec.decode(bin.toBitVector).require.value + val c = ChannelCodecs.channelDataCodec.decode(bin.toBitVector).require.value val ref = Seq( hex"304502210097fcda40b22916b5d61badedf6126658c2b5927d5002cc2c3e5f88a78ba5f45b02204a74bcf8827d894cab153fc051f39d8e2aeb660162a6a05797f7140587a6133301", @@ -68,7 +72,13 @@ class ChannelCodecsSpec extends AnyFunSuite { .htlcTxsAndRemoteSigs .map(data => Scripts.der(data.remoteSig)) - assert(ref === sigs) + assert(ref == sigs) + } + + test("nonreg for channel flags codec") { + // make sure that we correctly decode data encoded with the previous standard 'byte' codec + assert(CommonCodecs.channelflags.decode(byte.encode(0).require).require == DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + assert(CommonCodecs.channelflags.decode(byte.encode(1).require).require == DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) } test("backward compatibility DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec") { @@ -77,17 +87,17 @@ class ChannelCodecsSpec extends AnyFunSuite { // currently version=0 and discriminator type=1 assert(bin_old.startsWith(hex"000001")) // let's decode the old data (this will use the old codec that provides default values for new fields) - val data_new = stateDataCodec.decode(bin_old.toBitVector).require.value - assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) + val data_new = channelDataCodec.decode(bin_old.toBitVector).require.value + assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx == None) assert(TimestampSecond.now().toLong - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince.toLong < 3600) // we just set this to current time // and re-encode it with the new codec - val bin_new = ByteVector(stateDataCodec.encode(data_new).require.toByteVector.toArray) + val bin_new = ByteVector(channelDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format assert(bin_new.startsWith(hex"030000")) // now let's decode it again - val data_new2 = stateDataCodec.decode(bin_new.toBitVector).require.value + val data_new2 = channelDataCodec.decode(bin_new.toBitVector).require.value // data should match perfectly - assert(data_new === data_new2) + assert(data_new == data_new2) } test("backward compatibility DATA_NORMAL_COMPAT_03_Codec (roundtrip)") { @@ -111,40 +121,70 @@ class ChannelCodecsSpec extends AnyFunSuite { // check that this data has been encoded with the old 0x03 codec assert(oldbin.startsWith(hex"000003")) // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value + val oldnormal = channelDataCodec.decode(oldbin.bits).require.value // and we encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes + val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that encoding used the new codec - assert(newbin.startsWith(hex"030007")) + assert(newbin.startsWith(hex"030009")) // make sure that round-trip yields the same data - val newnormal = stateDataCodec.decode(newbin.bits).require.value - assert(newnormal === oldnormal) + val newnormal = channelDataCodec.decode(newbin.bits).require.value + assert(newnormal == oldnormal) } } test("backward compatibility older codecs (integrity)") { - // this test makes sure that we actually produce the same objects than previous versions of eclair - val refs = Map( - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", - hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" - ) + // It's not enough to just verify that codecs migrate without errors, we also need to make sure that the decoded + // data is correct. To do that, we compare json-serialized representations of the data. + + // NB: If this test fails (it will fail if there was a change in the model), follow this method which will save you + // a lot of time: + // 1) set 'debug = true' + // 2) run the test + // 3) under test/resources/nonreg/codecs/// you will find a file 'data.tmp.json' + // 4) use intellij to compare 'data.json' and 'data.tmp.json' and fix data.json + // 5) repeat until all test cases are fixed + // 6) make sure all 'data.tmp.json' have been cleaned up (they will be if the test passes) + // 7) set 'debug = false' + + val debug = false + + case class TestCase(dir: File, bin: ByteVector, json: String) + + val testCases = for { + // there are two level of directories for organization purposes + parentDir <- new File(getClass.getResource(s"/nonreg/codecs").getFile).listFiles() + dir <- parentDir.listFiles() + } yield { + val srcBin = Source.fromFile(new File(dir, "data.bin")) + val srcJson = Source.fromFile(new File(dir, "data.json")) + try { + TestCase(dir, ByteVector.fromValidHex(srcBin.mkString), srcJson.mkString) + } finally { + srcBin.close() + srcJson.close() + } + } - refs.foreach { case (oldbin, refjson) => - // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value - // we then encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes - // and we decode with the new codec - val newnormal = stateDataCodec.decode(newbin.bits).require.value - // finally we check that the actual data is the same as before (we just remove the new json field) - val oldjson = Serialization.write(oldnormal)(JsonSerializers.formats) - val newjson = Serialization.write(newnormal)(JsonSerializers.formats) - assert(oldjson === refjson) - assert(newjson === refjson) + testCases.foreach { testCase => + withClue(testCase.dir.getParentFile.getName + "/" + testCase.dir.getName) { + // we decode with compat codec + val olddecoded = channelDataCodec.decode(testCase.bin.bits).require.value + // we check that the decoded data is the same as before + val oldjson = Serialization.writePretty(olddecoded)(JsonSerializers.formats) + val tmpjsonfile_opt = if (debug) { + val tmpjsonfile = new File(testCase.dir.getPath.replace("target\\test-classes", "src\\test\\resources"), "data.tmp.json") + Serialization.writePretty(olddecoded, new FileWriter(tmpjsonfile))(JsonSerializers.formats) + Some(tmpjsonfile) + } else None + assert(oldjson == testCase.json) + tmpjsonfile_opt.foreach(_.delete()) + // we then encode with new codec + val newencoded = channelDataCodec.encode(olddecoded).require.bytes + // and we decode with the new codec + val newdecoded = channelDataCodec.decode(newencoded.bits).require.value + // and we make sure that we obtain the same data + assert(olddecoded == newdecoded) + } } } @@ -162,45 +202,45 @@ class ChannelCodecsSpec extends AnyFunSuite { // check that this data has been encoded with the old codec assert(oldBin.startsWith(hex"0100")) // we decode with the new codec - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value // and we encode with the new codec - val newBin = stateDataCodec.encode(decoded1).require.bytes + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0300")) // make sure that round-trip yields the same data - val decoded2 = stateDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + val decoded2 = channelDataCodec.decode(newBin.bits).require.value + assert(decoded1 == decoded2) }) - val negotiating = stateDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] + val negotiating = channelDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] assert(negotiating.bestUnpublishedClosingTx_opt.nonEmpty) - negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput === None)) + negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput == None)) assert(negotiating.closingTxProposed.flatten.nonEmpty) - negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput === None)) + negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput == None)) - val normal = stateDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] + val normal = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] assert(normal.commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - normal.commitments.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId === 0)) + normal.commitments.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId == 0)) - val closingLocal = stateDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] + val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingLocal.localCommitPublished.nonEmpty) - assert(closingLocal.localCommitPublished.get.commitTx.txOut.size === 6) - assert(closingLocal.localCommitPublished.get.htlcTxs.size === 4) - assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size === 4) + assert(closingLocal.localCommitPublished.get.commitTx.txOut.size == 6) + assert(closingLocal.localCommitPublished.get.htlcTxs.size == 4) + assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size == 4) assert(closingLocal.localCommitPublished.get.irrevocablySpent.isEmpty) - val closingRemote = stateDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] + val closingRemote = channelDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRemote.remoteCommitPublished.nonEmpty) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size === 7) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount === AnchorOutputsCommitmentFormat.anchorAmount) === 2) - assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size === 3) + assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size == 7) + assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount == AnchorOutputsCommitmentFormat.anchorAmount) == 2) + assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size == 3) assert(closingRemote.remoteCommitPublished.get.irrevocablySpent.isEmpty) - val closingRevoked = stateDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] - assert(closingRevoked.revokedCommitPublished.size === 1) - assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size === 6) - assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size === 4) - assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size === 2) + val closingRevoked = channelDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] + assert(closingRevoked.revokedCommitPublished.size == 1) + assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size == 6) + assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size == 4) + assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) assert(closingRevoked.revokedCommitPublished.head.irrevocablySpent.isEmpty) } @@ -222,12 +262,12 @@ class ChannelCodecsSpec extends AnyFunSuite { oldbins.foreach { oldbin => // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value + val oldnormal = channelDataCodec.decode(oldbin.bits).require.value // and we encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes + val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that round-trip yields the same data - val newnormal = stateDataCodec.decode(newbin.bits).require.value - assert(newnormal === oldnormal) + val newnormal = channelDataCodec.decode(newbin.bits).require.value + assert(newnormal == oldnormal) // make sure that we have stripped sigs from the transactions assert(newnormal.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty)) assert(newnormal.commitments.localCommit.htlcTxsAndRemoteSigs.forall(_.htlcTx.tx.txIn.forall(_.witness.stack.isEmpty))) @@ -247,20 +287,20 @@ object ChannelCodecsSpec { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - channelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 10000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, walletStaticPaymentBasepoint = None, - isFunder = true, + isInitiator = true, initFeatures = Features.empty) val remoteParams: RemoteParams = RemoteParams( nodeId = randomKey().publicKey, dustLimit = 546 sat, maxHtlcValueInFlightMsat = UInt64(5000000), - channelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 5000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, @@ -310,7 +350,8 @@ object ChannelCodecsSpec { val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, commitTx), remoteSig), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(_.opposite).toSet, FeeratePerKw(1500 sat), 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes) - val commitments = Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + val channelFlags = ChannelFlags.Public + val commitments = Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, channelFlags, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, originChannels = origins, @@ -318,7 +359,7 @@ object ChannelCodecsSpec { commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init) - DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None, None) + DATA_NORMAL(commitments, ShortIds(RealScidStatus.Final(RealShortChannelId(42)), ShortChannelId.generateLocalAlias(), None), None, channelUpdate, None, None, None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala index c488ee882b..24fe9f5e89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair.wire.internal.channel.version0 -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0.Codecs._ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion @@ -22,21 +22,21 @@ class ChannelCodecs0Spec extends AnyFunSuite { val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current04 = hex"010000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(codec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) - assert(codec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) - assert(codec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) - assert(codec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) - assert(codec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) + assert(codec.decode(legacy02.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) + assert(codec.decode(legacy03.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) + assert(codec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) + assert(codec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) + assert(codec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) - assert(codec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000001".bits)) - assert(codec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"0100000003".bits)) + assert(codec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"0100000001".bits)) + assert(codec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"0100000003".bits)) } test("backward compatibility local params with global features") { // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"0a8a") + assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"0a8a") } test("backward compatibility of htlc codec") { @@ -69,8 +69,8 @@ class ChannelCodecs0Spec extends AnyFunSuite { assert(r1 == remaining) assert(r2 == remaining) - assert(codec.encode(h1).require.bytes === encodedHtlc1) - assert(codec.encode(h2).require.bytes === encodedHtlc2) + assert(codec.encode(h1).require.bytes == encodedHtlc1) + assert(codec.encode(h2).require.bytes == encodedHtlc2) } test("tell channel_update length") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index bcd1c32adc..545da0fdc5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -2,9 +2,9 @@ package fr.acinq.eclair.wire.internal.channel.version1 import akka.actor.ActorSystem import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{LocalParams, Origin, RemoteParams} import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} @@ -25,14 +25,14 @@ class ChannelCodecs1Spec extends AnyFunSuite { val keyPath = KeyPath(Seq(0L, 0L, 0L, 0L)) val encoded = keyPathCodec.encode(keyPath).require val decoded = keyPathCodec.decode(encoded).require - assert(keyPath === decoded.value) + assert(keyPath == decoded.value) } test("encode/decode key paths (all 1s)") { val keyPath = KeyPath(Seq(0xffffffffL, 0xffffffffL, 0xffffffffL, 0xffffffffL)) val encoded = keyPathCodec.encode(keyPath).require val decoded = keyPathCodec.decode(encoded).require - assert(keyPath === decoded.value) + assert(keyPath == decoded.value) } test("encode/decode channel version") { @@ -41,21 +41,21 @@ class ChannelCodecs1Spec extends AnyFunSuite { val current04 = hex"0000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current05 = hex"0000000703d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) - assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) - assert(channelVersionCodec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) - assert(channelVersionCodec.decode(current05.bits) === Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) + assert(channelVersionCodec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) + assert(channelVersionCodec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) + assert(channelVersionCodec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) + assert(channelVersionCodec.decode(current05.bits) == Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) - assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"00000001".bits)) - assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"00000003".bits)) - assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) === Attempt.successful(hex"00000007".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"00000001".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"00000003".bits)) + assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) == Attempt.successful(hex"00000007".bits)) } - test("encode/decode localparams") { + test("encode/decode local params") { def roundtrip(localParams: LocalParams, codec: Codec[LocalParams]) = { val encoded = codec.encode(localParams).require val decoded = codec.decode(encoded).require - assert(localParams === decoded.value) + assert(localParams == decoded.value) } val o = LocalParams( @@ -63,14 +63,14 @@ class ChannelCodecs1Spec extends AnyFunSuite { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)), walletStaticPaymentBasepoint = None, - isFunder = Random.nextBoolean(), - initFeatures = Features(randomBytes(256))) + isInitiator = Random.nextBoolean(), + initFeatures = Features(randomBytes(256)).initFeatures()) val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32()).publicKey)) roundtrip(o, localParamsCodec(ChannelVersion.ZEROES)) @@ -78,12 +78,12 @@ class ChannelCodecs1Spec extends AnyFunSuite { roundtrip(o, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) } - test("encode/decode remoteparams") { + test("encode/decode remote params") { val o = RemoteParams( nodeId = randomKey().publicKey, dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -92,16 +92,16 @@ class ChannelCodecs1Spec extends AnyFunSuite { paymentBasepoint = randomKey().publicKey, delayedPaymentBasepoint = randomKey().publicKey, htlcBasepoint = randomKey().publicKey, - initFeatures = TestConstants.Alice.nodeParams.features, + initFeatures = TestConstants.Alice.nodeParams.features.initFeatures(), shutdownScript = None) val encoded = remoteParamsCodec.encode(o).require val decoded = remoteParamsCodec.decodeValue(encoded).require - assert(o === decoded) + assert(o == decoded) - // Backwards-compatibility: decode remoteparams with global features. + // Backwards-compatibility: decode remote params with global features. val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"028a") + assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"028a") } test("encode/decode htlc") { @@ -114,8 +114,8 @@ class ChannelCodecs1Spec extends AnyFunSuite { onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = IncomingHtlc(add) val htlc2 = OutgoingHtlc(add) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require === htlc2) + assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require == htlc1) + assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require == htlc2) } test("encode/decode commitment spec") { @@ -136,7 +136,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { val htlc1 = IncomingHtlc(add1) val htlc2 = OutgoingHtlc(add2) val htlcs = Set[DirectedHtlc](htlc1, htlc2) - assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require === htlcs) + assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require == htlcs) val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), commitTxFeerate = FeeratePerKw(Random.nextInt(Int.MaxValue).sat), @@ -145,7 +145,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { ) val encoded = commitmentSpecCodec.encode(o).require val decoded = commitmentSpecCodec.decode(encoded).require - assert(o === decoded.value) + assert(o == decoded.value) } test("encode/decode origin") { @@ -153,14 +153,14 @@ class ChannelCodecs1Spec extends AnyFunSuite { val localHot = Origin.LocalHot(replyTo, UUID.randomUUID()) val localCold = Origin.LocalCold(localHot.id) - assert(originCodec.decodeValue(originCodec.encode(localHot).require).require === localCold) - assert(originCodec.decodeValue(originCodec.encode(localCold).require).require === localCold) + assert(originCodec.decodeValue(originCodec.encode(localHot).require).require == localCold) + assert(originCodec.decodeValue(originCodec.encode(localCold).require).require == localCold) val add = UpdateAddHtlc(randomBytes32(), 4324, 11000000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket) val relayedHot = Origin.ChannelRelayedHot(replyTo, add, 11000000 msat) val relayedCold = Origin.ChannelRelayedCold(add.channelId, add.id, add.amountMsat, relayedHot.amountOut) - assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require === relayedCold) - assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require === relayedCold) + assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require == relayedCold) + assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require == relayedCold) val adds = Seq( UpdateAddHtlc(randomBytes32(), 1L, 1000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket), @@ -169,8 +169,8 @@ class ChannelCodecs1Spec extends AnyFunSuite { ) val trampolineRelayedHot = Origin.TrampolineRelayedHot(replyTo, adds) val trampolineRelayedCold = Origin.TrampolineRelayedCold(trampolineRelayedHot.htlcs) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require === trampolineRelayedCold) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedCold).require).require === trampolineRelayedCold) + assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require == trampolineRelayedCold) + assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedCold).require).require == trampolineRelayedCold) } test("encode/decode map of origins") { @@ -184,7 +184,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { -32L -> Origin.ChannelRelayedCold(randomBytes32(), 54, 15000000 msat, 14000000 msat), -54L -> Origin.TrampolineRelayedCold((randomBytes32(), 1L) :: (randomBytes32(), 2L) :: Nil), -4L -> Origin.LocalCold(UUID.randomUUID())) - assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) + assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require == map) } test("encode/decode map of spending txes") { @@ -194,7 +194,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { OutPoint(randomBytes32(), 0) -> randomBytes32(), OutPoint(randomBytes32(), 454513) -> randomBytes32() ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) + assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala index 4fddb6e55c..ca95bd323e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala @@ -1,9 +1,9 @@ package fr.acinq.eclair.wire.internal.channel.version2 -import fr.acinq.bitcoin.{ByteVector64, OutPoint, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, Transaction} import fr.acinq.eclair.channel.{ChannelConfig, ChannelFeatures, HtlcTxAndRemoteSig} import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.channelDataCodec import fr.acinq.eclair.{FeatureSupport, Features, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.HexStringSyntax @@ -20,11 +20,11 @@ class ChannelCodecs2Spec extends AnyFunSuite { OutPoint(randomBytes32(), 0) -> Transaction.read("02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a040000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef48347304402205bcfa92f83c69289a412b0b6dd4f2a0fe0b0fc2d45bd74706e963257a09ea24902203783e47883e60b86240e877fcbf33d50b1742f65bc93b3162d1be26583b367ee012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000"), OutPoint(randomBytes32(), 454513) -> Transaction.read("02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b00000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b014730440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000") ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) + assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) } test("remove signatures from commitment txs") { - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) assert(commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) commitments.localCommit.htlcTxsAndRemoteSigs.foreach { @@ -37,33 +37,33 @@ class ChannelCodecs2Spec extends AnyFunSuite { test("split channel version into channel config and channel features") { { // Standard channel - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures()) + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures()) } { val staticRemoteKeyChannel = hex"000200000003039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500092eaee758da6a5beab04167c5b889732a98ec556cf92689da41744b10159ace488000000000000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e001600140a46fc26a4ef7b50aaf16759d2f0ca8b014fd0a6028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000000302698203af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000000000000044c000000001dcd6500000000000000271000000000000000000090006402a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa838730328e2fd423323abcd86b15335ff44679068acd4db71b8052f63a2a7f0b47432bd028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12025ee0984b83efe2369c23c3754e89d19905f8094a86486de3b241cf8735208292031163dfd4eee879b9a333290cf10e52eb010d1da45bd44efee6fab16d1035120c0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269820000000000000000010001fffd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000000bebc200000000002cb4178024b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aefd018602000000000101b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000000036a2ab800350c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e30400d030000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831cb04e0b0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30400483045022100dc11d7761cdf7c708ea2aaaed409a4d24697bb07948704e77b7cbe911d0268fd02204a81721e85bb1ff6cb34741ed5c5ee89c4ea8f5bb1515792625d32043682c14301483045022100af2d760500664483ea81ccaa3bc925c5587cd3cd4358ee27110f62929903c49602201ce1a97498227eaef1ab5c04da2eb9ea8114abe6af1bd35494d9cbb0e64da3e7014752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aead678920000101241bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf731487000000002b50c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e308b76a91420fe4794a7f993489c0477c442fed3be2c9294678763ac67210230714b6385889be70b5299603fbc508a01517a3b754d03e33e5bcf125104adc57c8201208763a914e5a9c4cf993e5053ca47ddd2a194a3c522c1fdc588527c2103bff704aae937cd0b98704a4791140cab835c5bd2406897104602c881352869ae52ae677503101b06b175ac68685e02000000011bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf73148700000000000000000001daa7000000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831c00000000a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e000000000000000040c5c678fda30c1e23bbb68093b1d9a34169304ff3d36bfef74157f129df8d01e31ddcd8dd04f3ac9521ac13b7fed5d7997ce0c72c942054711d924ea197c7ab7640cd0045f41ffd38fa637cfc09679c628b29a8273223db1150e262d5159cf5d7852542c4ad7334f67d08ca835cd5b16b1df96544c454dff121856685471d2255300000000000000001000100fd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000002cb41780000000000bebc200b5c42b5c67eb6043b2776c0e9a8bd91d8ffdf7280b81dce825558ad5258746fb030d9b4186ad4f32aae9ca90701dc66e8df3f7b73e1f9590c0f6cd690aae8ad097000000000000000000000000000000000000000000000000000000010000ff0250b3cac5267f9448efcb8c2a89944f8bb4e4e7f1a3ae72d4961dfe187105b3f324b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352ae000100400000ffffffffffff0020b0e394cbe4d79e47a73926b89d17d8835e8f042d7adb244a84b6c30496c6355f80007fffffffffff80b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072061a8000002a00000000888bc37730827b0ccb54f13689babb8d211a6cbbec55113caad14a97f6bfce7d85313c9cefa398d15213abf841e3c9a8b88f62fa3fc742a6c746619d3e76aa71fb06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32b8d010000900000000000000000000854d00000000a000000003b9aca000000" - val commitments = stateDataCodec.decode(staticRemoteKeyChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey)) + val commitments = channelDataCodec.decode(staticRemoteKeyChannel.bits).require.value.commitments + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) } { val anchorOutputsChannel = hex"00020000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094b82816a3f15ab3e324ddf6f0f5a116cef8c3200a353240db684337d2dcaa81e80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014f09f6f93de60c6af417ed31d52c2c883b6113f260000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000225982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d5002eb112dd8d61a02bf3434d5417e1020ed2d951e75bd1e14615d473dd5f1991b4402ca8b6891d6a53aea035259fa0d57a1e672d3cc59a2c696a96247d3557ee726cd030f2aa0ca0ae0e00d6da847d897cf7288ae286dd12037f06250937dc7b37fe0b5028d59e68c5799980a98b120acb891dabfd091e80c6596b0c2a7de57a298d48512000000032259820000000000000000000000000009c4000000002faf0800000000000bebc2002432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352aefd01bc0200000000010132e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000000089273f80044a010000000000002200202102815a0b14d50a2a8c028cd47413e743f02ad5460151bd4ae5930639cbbd484a010000000000002200204e81bd6470c45c349068c2b0ad544934b718ef49c1615f2a0b0cd8766400b562400d0300000000002200208eadab92c95168438acfb73f2d3d92f56d6fc30d3f79fc4527f4e77df88b8add72270c0000000000220020895c0fef7efb7aa0e4805381434363b30386d31f6cb621abb0ca4a52b269fbc304004830450221009f09a584b1af9ba86f1618aad1ca9817324f97c3d3789b2490c3b9435c7bf1dd02205e4dcca3d7937f6b80a0ed39084e4ea9e9c531b80698ebd0c116fd9319a0db1201473044022001e4104ec25ba9bc43bf3f623d6e98f371434442b134255667ddeedd9cd9cd1b02201f057d2c15ee4e5d62a73f1db262846ca7ad35deaed2496abdb5b16cd91c5cb80147522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae25899520000000000000000000000000000009c4000000000bebc200000000002faf0800acdb0bf90ed7350d429c2d70fce66300ac9b27d8dbb3bceb5bd2cc8bd443938f03f54d00601f1b95e82836540d760ab6b38bc6fc1701df5d5e3b410f1936212d54000000000000000000000000000000000000000000000000000000000000ff03658b1e02efd8464688f87a052518691ff4d818a7c36addf4c6520ae07b7e20912432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae00000032e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63061a8000002a000000008833a9275befbedbf928e743a71c9476f46528baf6e832ad25478c2607e4108dea67657c51db7344032627c855518179dc7a94b59f306faf9a3b4eef7684d5d73106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32a590101009000000000000003e8000854d00000000a000000003b9aca000000" - val commitments = stateDataCodec.decode(anchorOutputsChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + val commitments = channelDataCodec.decode(anchorOutputsChannel.bits).require.value.commitments + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } { val wumboChannel = hex"00000000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009b3b6da18b7e5e43405415a43e61abd77edf4d300c131af17a955950837d7db4980000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600142946bdbb3141be9a40ed8133ef159ee0022176e90000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b81607025c24f0c3572631ea09dade913ccbb1cd8a55585fc152cc2165bf4f43af702c8b0306c241c667eefee031639052c4a35c4ae076ab56b49f76f7e12f63dfb7bb32a503fdbff1ca92c99c09a9c0eac3cfe1ca35deb658d5fdb815fe7a240df25da5e20f02c47d9e4e1fb501c81933cfc4d3ba7f0e8203003cda2683569f1f67aac2b6e20d000000030a4982000000000000000000000000002710000000745e66c600000000000bebc2002464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aefd01590200000000010164fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000000018e2678002400d030000000000160014cf3c9d08ec27d3ffefbf7e2640d3773e11a60ceb783bca1d00000000220020153e514ade07f347db9ed32cf6b2096d066d48e0cf8c504590cd1155de68be2204004730440220056ac83d5b2eb9eb9714e85294d080a9b64f73916d8206b15533613b4303ed3d022044da40ba433e27ddb8743c75acc0f6b050c346bc9d241ab3a1b65051820ce06a0147304402207683c10f33682e2389e5f02dd34090bbe4d786633d49e1777d5ec2ab71a7924902206d99dceb74ff809237ae8e06c118b38047e2d4a49f9956c538bedca47cb4b1da0147522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aef5c4602000000000000000000000000000002710000000000bebc200000000745e66c6000bdebd6cf39db4c2ce1a675281b0a714e525525149032ba048d75c89e5a9b355021a87b53e12e4b36287635cdf887991a732c2fd56b12f5377d755b8b890795db6000000000000000000000000000000000000000000000000000000000000ff032c5f941f78a00105b995fba7f3c340afe53e287a60361135386ae1448d6f3d632464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752ae00000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32cff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff010065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b9810000000000000000060e32bf9000082000000000000000000000000000000000000000000000000000000000000000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c0000f4957823b0a6b76697d7c545bcca66abec8522a0f6350a722e45f3f2830f7f824c84efed8daffa04bb0822ce1a08fb2de77dd20c23a91cced7a3966808956644" - val commitments = stateDataCodec.decode(wumboChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.Wumbo)) + val commitments = channelDataCodec.decode(wumboChannel.bits).require.value.commitments + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.Wumbo)) } } test("ensure remote shutdown script is not set") { - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments - assert(commitments.remoteParams.shutdownScript === None) + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments + assert(commitments.remoteParams.shutdownScript == None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index b6fc95aa4e..7636977a31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -15,39 +15,41 @@ */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.stateDataCodec -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} +import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.channelDataCodec +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} +import scala.util.Random + class ChannelCodecs3Spec extends AnyFunSuite { test("basic serialization test (NORMAL)") { val data = normal - val bin = DATA_NORMAL_Codec.encode(data).require - val check = DATA_NORMAL_Codec.decodeValue(bin).require - assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec) - assert(data === check) + val bin = DATA_NORMAL_09_Codec.encode(data).require + val check = DATA_NORMAL_09_Codec.decodeValue(bin).require + assert(data.commitments.localCommit.spec == check.commitments.localCommit.spec) + assert(data == check) } test("encode/decode channel configuration options") { - assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes === hex"00") - assert(channelConfigCodec.decode(hex"00".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"01f0".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"020000".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - - assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes === hex"0101") - assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes === hex"0101") - assert(channelConfigCodec.decode(hex"0101".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"01ff".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"020001".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes == hex"00") + assert(channelConfigCodec.decode(hex"00".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + assert(channelConfigCodec.decode(hex"01f0".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + assert(channelConfigCodec.decode(hex"020000".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + + assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes == hex"0101") + assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes == hex"0101") + assert(channelConfigCodec.decode(hex"0101".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.decode(hex"01ff".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.decode(hex"020001".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) } test("decode all known channel configuration options") { @@ -73,15 +75,16 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(declaredOptions.nonEmpty) val encoded = channelConfigCodec.encode(ChannelConfig(declaredOptions)).require val decoded = channelConfigCodec.decode(encoded).require.value - assert(decoded.options === declaredOptions) + assert(decoded.options == declaredOptions) } test("encode/decode optional shutdown script") { + val codec = remoteParamsCodec(ChannelFeatures()) val remoteParams = RemoteParams( randomKey().publicKey, Satoshi(600), UInt64(123456L), - Satoshi(300), + Some(Satoshi(300)), MilliSatoshi(1000), CltvExpiryDelta(42), 42, @@ -92,37 +95,85 @@ class ChannelCodecs3Spec extends AnyFunSuite { randomKey().publicKey, Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), None) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams).require).require === remoteParams) + assert(codec.decodeValue(codec.encode(remoteParams).require).require == remoteParams) val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams1).require).require === remoteParams1) + assert(codec.decodeValue(codec.encode(remoteParams1).require).require == remoteParams1) val dataWithoutRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value === dataWithoutRemoteShutdownScript) + assert(DATA_NORMAL_09_Codec.decode(DATA_NORMAL_09_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value == dataWithoutRemoteShutdownScript) val dataWithRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams1)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value === dataWithRemoteShutdownScript) + assert(DATA_NORMAL_09_Codec.decode(DATA_NORMAL_09_Codec.encode(dataWithRemoteShutdownScript).require).require.value == dataWithRemoteShutdownScript) + } + + test("encode/decode optional channel reserve") { + val localParams = LocalParams( + randomKey().publicKey, + DeterministicWallet.KeyPath(Seq(42L)), + Satoshi(660), + UInt64(500000), + Some(Satoshi(15000)), + MilliSatoshi(1000), + CltvExpiryDelta(36), + 50, + Random.nextBoolean(), + hex"deadbeef", + None, + Features().initFeatures()) + val remoteParams = RemoteParams( + randomKey().publicKey, + Satoshi(500), + UInt64(100000), + Some(Satoshi(30000)), + MilliSatoshi(1500), + CltvExpiryDelta(144), + 10, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + Features(), + None) + + { + val localCodec = localParamsCodec(ChannelFeatures()) + val remoteCodec = remoteParamsCodec(ChannelFeatures()) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams == localParams) + assert(decodedRemoteParams == remoteParams) + } + { + val localCodec = localParamsCodec(ChannelFeatures(Features.DualFunding)) + val remoteCodec = remoteParamsCodec(ChannelFeatures(Features.DualFunding)) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams == localParams.copy(requestedChannelReserve_opt = None)) + assert(decodedRemoteParams == remoteParams.copy(requestedChannelReserve_opt = None)) + } } test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates === None) - val newBin = stateDataCodec.encode(decoded1).require.bytes + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value + assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates == None) + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec - assert(newBin.startsWith(hex"0007")) - val decoded2 = stateDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + assert(newBin.startsWith(hex"0009")) + val decoded2 = channelDataCodec.decode(newBin.bits).require.value + assert(decoded1 == decoded2) } test("backward compatibility DATA_SHUTDOWN_COMPAT_03_Codec") { val oldBin = hex"0003342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009f2408e50889ac3f6c19a007e25752bf143ab0d920cedb9f1c7b7b6b2270c1fe080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014d0cd80743458194e5f86d9a74592765442fd15bb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c50223dc803cceb3edff5deacc9965002cc366b719331e776abf76ee680e4afbf46a0315880bf27110fa57f5b75607bc32a098c209fe8a5a2a30120b2a246fd3fcd62e025dc435be4cb790e5ab12705fb3c0b717ddd9f4d0234fdb557358ecefc6a4e97a0238e3723a9d60dac60c2673ec836e2a909ba9fc888fcbd1cd2ad57ff0b10c5c360000000302498200000000000000000001000200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d2200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e000027100000000011e1a300000000000bebc20024342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552aed30200000001342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000007d30f88004400d030000000000160014dab249c7d3b8d826d45244f609bb90f72e691daa400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee286a04000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394ee093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d1b17b4207914784cfd82df8f93f15defff862cede27811ce4c59df98e86fd57e5b5105921e842a02a47ee6ddb313b37bc0b6706fe3011e2dd2d82066c85ab11a8d7b4e47000202241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000002b400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a9142a784c1850f79298eab62c5c7709a5d468f5522788ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000000000000000015af302000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000001f8c46e9fa497a4f9f4fdf422840d264f67178defb43320b83c1d9825a6c152995775652ba3689025f4841f165acbd16732ad88629429b3811d71c7b03adf254402241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313030000002be093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a91448d436723875e511f3a4fc540ca63c91637e926388ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de5431303000000000000000001fa7904000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000000ce718c1999b28ddd1207bcdd6789541ec2f405f775efb096bd896bb237b7f5c17543684f99cb53efeade77bc386997e7b5361418c510ed442700cee38c36f18900000000000000010002fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d22fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e00002710000000000bebc2000000000011e1a300306acd0b21100f755a6d193a819042fde1b4c3839598e730905dfa7e6ef699220240a9cccdb58d887b42ccadeee5704030acd52bd9c7759ba01f47f0995531ca360000000000000000000000000000000000000002000000000000000000020000000000000000000334c7b350311d40b3b422e07f3b7759b400000000000000010003e3dd409b342046e7b410cc380c9a09c2ff03cdeeaf422678a9eac407da8f811a402718083016f88d5404f5508054fe25acb824342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552ae000100400000ffffffffffff00204cec06f1f1496c01cd4840cddff19b47c24b7daa478bcaea862c6d11fcc9fff180007fffffffffff8038342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300160014d0cd80743458194e5f86d9a74592765442fd15bb38342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30016001483366b494906052362a88e967304296937926cd1" - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates === None) - val newBin = stateDataCodec.encode(decoded1).require.bytes + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value + assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates == None) + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0008")) - val decoded2 = stateDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + val decoded2 = channelDataCodec.decode(newBin.bits).require.value + assert(decoded1 == decoded2) } test("backwards compatibility with legacy claim-htlc-success transactions") { @@ -132,7 +183,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { val legacyClaimHtlcSuccess = { val claimHtlcSuccessTx1 = txWithInputInfoCodec.decode(oldTxWithInputInfoCodecBin.bits).require.value val claimHtlcSuccessTx2 = claimHtlcTxCodec.decode(oldClaimHtlcTxBin.bits).require.value - assert(claimHtlcSuccessTx1 === claimHtlcSuccessTx2) + assert(claimHtlcSuccessTx1 == claimHtlcSuccessTx2) assert(claimHtlcSuccessTx1.isInstanceOf[LegacyClaimHtlcSuccessTx]) claimHtlcSuccessTx1.asInstanceOf[LegacyClaimHtlcSuccessTx] } @@ -146,9 +197,9 @@ class ChannelCodecs3Spec extends AnyFunSuite { // And decode new data that contains a payment hash. val decoded1 = txWithInputInfoCodec.decode(txWithInputInfoCodecBin.bits).require.value - assert(claimHtlcSuccess === decoded1) + assert(claimHtlcSuccess == decoded1) val decoded2 = claimHtlcTxCodec.decode(claimHtlcTxBin.bits).require.value - assert(claimHtlcSuccess === decoded2) + assert(claimHtlcSuccess == decoded2) } test("backwards compatibility with transactions missing a confirmation target") { @@ -156,46 +207,66 @@ class ChannelCodecs3Spec extends AnyFunSuite { val oldAnchorTxBin = hex"0011 24bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b8000000002b4a0100000000000022002036c067df8952dbcd5db347e7c152ca3fa4514f2072d27867837b1c2d319a7e01282103cc89f1459b5201cda08e08c6fb7b1968c54e8172c555896da27c6fdc10522ceeac736460b268330200000001bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b80000000000000000000000000000" val oldAnchorTx = txWithInputInfoCodec.decode(oldAnchorTxBin.bits).require.value assert(oldAnchorTx.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].confirmBefore === BlockHeight(0)) + assert(oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].confirmBefore == BlockHeight(0)) val anchorTx = oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmBefore = BlockHeight(1105)) val anchorTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(anchorTx).require).require.value - assert(anchorTx === anchorTx2) + assert(anchorTx == anchorTx2) } { val oldHtlcSuccessTxBin = hex"0002 24f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000002bb0ad010000000000220020462cf8912ffc5f27764c109bed188950500011a2837ff8b9c8f9a39cffa395a58b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac6868fd01a002000000000101f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000000000000000013a920100000000002200208742b16c9fd4e74854dcd84322dd1de06f7993fe627fd2ca0be4b996a936d56b050047304402201b4527c8f420852550af00bbd9149db9b31adcb7e1f127766e75e1e01746df0302202a57bb1e274ed7d3e8dbe5f205de721a23092c1e2ce2135f4750f18f6c0b51b001483045022100b6df309c8e5746a077b1f7c2f299528e164946bd514a5049475af7f5665805da0220392ae877112a3c52f74d190b354b4f5c020da9c1a71a7a08ced0a5363e795a27012017ea8f5afde8f708258d5669e1bbd454e82ddca8c6c480ec5302b4b1e8051d3d8b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac686800000000dc7002a387673f17ebaf08545ccec712a9b6914813cdb83b4270932294f20f660000000000000000" val oldHtlcSuccessTx = txWithInputInfoCodec.decode(oldHtlcSuccessTxBin.bits).require.value assert(oldHtlcSuccessTx.isInstanceOf[HtlcSuccessTx]) - assert(oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].confirmBefore === BlockHeight(0)) + assert(oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].confirmBefore == BlockHeight(0)) val htlcSuccessTx = oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].copy(confirmBefore = BlockHeight(1105)) val htlcSuccessTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(htlcSuccessTx).require).require.value - assert(htlcSuccessTx === htlcSuccessTx2) + assert(htlcSuccessTx == htlcSuccessTx2) } { val oldHtlcTimeoutTxBin = hex"0003 248f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000002b50c300000000000022002005ff644937d7f5f32ec194424f551371e8d4bcf2cda3e1096cdd2fe88687fc408576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868fd0179020000000001018f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000000000000000016aa9000000000000220020c980a1573ce6dc6a1bb8a1d60ecffdf2f0a7aa983c49786a2ab1ba8f0cd74a76050047304402200d2b631fb0e5a7f406f3de2b36fe3c0ab3337fe98f114dadf38de8f5548e87eb02203ad7c385c7cf62ac17ec15329dee071ee2c479aceb34a14d700dfeba80008574014730440220653b4ff490b03de06a9053aa7ab0b30e85c484c76900c586db65f7308f38ad86022019ac12ffb127e4a17a01dc6db730d3eccea837d2dc85a0275bdea8b393a2f11d01008576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868101b06000000000000000003" val oldHtlcTimeoutTx = txWithInputInfoCodec.decode(oldHtlcTimeoutTxBin.bits).require.value assert(oldHtlcTimeoutTx.isInstanceOf[HtlcTimeoutTx]) - assert(oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].confirmBefore === BlockHeight(0)) + assert(oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].confirmBefore == BlockHeight(0)) val htlcTimeoutTx = oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].copy(confirmBefore = BlockHeight(1105)) val htlcTimeoutTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(htlcTimeoutTx).require).require.value - assert(htlcTimeoutTx === htlcTimeoutTx2) + assert(htlcTimeoutTx == htlcTimeoutTx2) } { val oldClaimHtlcSuccessTxBin = hex"0016 24e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d89000000002bb0ad01000000000022002063e22369052a2bad9eb124737742690b8d1aba7693869d041da16443e2973e638576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac6868fd014502000000000101e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d890000000000000000000162970100000000001600140262586eef1a2c8f47ebc139a2733123d09e315603483045022100898f6b51361f044c8c54468dcb1b9decd75edcc1b69b62e584059b512eb075900220675f8b23aa402bb7d5bfdefbac4074088f82bd34d09b34c651c0dff48e6967930120efb38d645311af59c028cd8a9bf8ee21ff9e7c1a1cff1a0398a0315280247ac38576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac686800000000ad02394dd18774f6f403f783afb518b6c69a89d531f718b29868ddca3e7905020000000000000000" val oldClaimHtlcSuccessTx = txWithInputInfoCodec.decode(oldClaimHtlcSuccessTxBin.bits).require.value assert(oldClaimHtlcSuccessTx.isInstanceOf[ClaimHtlcSuccessTx]) - assert(oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].confirmBefore === BlockHeight(0)) + assert(oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].confirmBefore == BlockHeight(0)) val claimHtlcSuccessTx = oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].copy(confirmBefore = BlockHeight(1105)) val claimHtlcSuccessTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(claimHtlcSuccessTx).require).require.value - assert(claimHtlcSuccessTx === claimHtlcSuccessTx2) + assert(claimHtlcSuccessTx == claimHtlcSuccessTx2) } { val oldClaimHtlcTimeoutTxBin = hex"0005 24df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d8000000002b50c30000000000002200205db892d76fb358ed508ec09c77b5a184cd9de3aa3e74a025a5c5d3f7adc221f78b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868fd012b02000000000101df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d80000000000000000000106ae0000000000001600142b8e221121004b248f6551a0c6fb8ce219f4997e03483045022100ecdb8179f92c097594844756ac2bd7948f1c44ae74b54dfda86971800e59bf980220125edbe563f24cde15fa1fa25f4eac7cb938a3ace6f9329eb487938005c7621501008b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868981a06000000000000000003" val oldClaimHtlcTimeoutTx = txWithInputInfoCodec.decode(oldClaimHtlcTimeoutTxBin.bits).require.value assert(oldClaimHtlcTimeoutTx.isInstanceOf[ClaimHtlcTimeoutTx]) - assert(oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].confirmBefore === BlockHeight(0)) + assert(oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].confirmBefore == BlockHeight(0)) val claimHtlcTimeoutTx = oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].copy(confirmBefore = BlockHeight(1105)) val claimHtlcTimeoutTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(claimHtlcTimeoutTx).require).require.value - assert(claimHtlcTimeoutTx === claimHtlcTimeoutTx2) + assert(claimHtlcTimeoutTx == claimHtlcTimeoutTx2) + } + } + + test("backwards compatibility with codecs pre-alias") { + { + val bin = hex"0001000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae00000000000000075bcd1541dd93fe76288e183498a7d57065cf472a4413c643730f9c117ca806be6b57f75303a153d48f51439f168ca85ef2fd6f3e0642fc7132f7a530cd50d880ec05cb4c17" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY] + assert(data.shortIds.localAlias == ShortChannelId(123456789L)) + assert(data.shortIds.real == RealScidStatus.Temporary(RealShortChannelId(123456789L))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("000a")) // NB: 01 -> 0a + } + + { + val bin = hex"0007000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000000000000000002aff008821c93fbf280b2391826bc70ae858cf815d4afc1816f85445364f188e635d4ae64dfe1c58bedb017dd6f267452444d991b66fcfc638396f72fa6926f69d6125ff01010101010101010101010101010101010101010101010101010101010101010000000000022cd962975eec0101002a000000000000000f0000023f0000003500000003e8000000000000" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(data.shortIds.localAlias == ShortChannelId(42)) + assert(data.shortIds.real == RealScidStatus.Final(RealShortChannelId(42))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("0009")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index 05fa6eb687..feeef78e46 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -17,15 +17,16 @@ package fr.acinq.eclair.wire.protocol import com.google.common.net.InetAddresses -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.{UInt64, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult -import scodec.bits.{BitVector, HexStringSyntax} +import scodec.bits.{BinStringSyntax, BitVector, HexStringSyntax} import scodec.codecs.uint32 import java.net.{Inet4Address, Inet6Address, InetAddress} @@ -46,9 +47,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((uint, ref) <- expected) { val encoded = uint64.encode(uint).require - assert(ref === encoded) + assert(ref == encoded) val decoded = uint64.decode(encoded).require.value - assert(uint === decoded) + assert(uint == decoded) } } @@ -59,7 +60,7 @@ class CommonCodecsSpec extends AnyFunSuite { UInt64(hex"efffffffffffffff"), UInt64(hex"effffffffffffffe") ) - assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value === value)) + assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value == value)) } test("encode/decode with varint codec") { @@ -77,9 +78,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((uint, ref) <- expected) { val encoded = varint.encode(uint).require - assert(ref === encoded, ref) + assert(ref == encoded, ref) val decoded = varint.decode(encoded).require.value - assert(uint === decoded, uint) + assert(uint == decoded, uint) } } @@ -120,9 +121,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((long, ref) <- expected) { val encoded = varintoverflow.encode(long).require - assert(ref === encoded, ref) + assert(ref == encoded, ref) val decoded = varintoverflow.decode(encoded).require.value - assert(long === decoded, long) + assert(long == decoded, long) } } @@ -137,12 +138,27 @@ class CommonCodecsSpec extends AnyFunSuite { } } + test("encode/decode channel flags") { + val testCases = Map( + bin"00000000" -> ChannelFlags(announceChannel = false), + bin"00000001" -> ChannelFlags(announceChannel = true), + ) + testCases.foreach { case (bin, obj) => + assert(channelflags.decode(bin).require == DecodeResult(obj, BitVector.empty)) + assert(channelflags.encode(obj).require == bin) + } + + // BOLT 2: The receiving node MUST [...] ignore undefined bits in channel_flags. + assert(channelflags.decode(bin"11111111").require == DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) + assert(channelflags.decode(bin"11111110").require == DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + } + test("encode/decode with rgb codec") { val color = Color(47.toByte, 255.toByte, 142.toByte) val bin = rgb.encode(color).require - assert(bin === hex"2f ff 8e".toBitVector) + assert(bin == hex"2f ff 8e".toBitVector) val color2 = rgb.decode(bin).require.value - assert(color === color2) + assert(color == color2) } test("encode/decode all kind of IPv6 addresses with ipv6address codec") { @@ -151,7 +167,7 @@ class CommonCodecsSpec extends AnyFunSuite { val bin = hex"00000000000000000000ffffae8a0b08".toBitVector val ipv6 = Inet6Address.getByAddress(null, bin.toByteArray, null) val bin2 = ipv6address.encode(ipv6).require - assert(bin === bin2) + assert(bin == bin2) } { @@ -159,7 +175,7 @@ class CommonCodecsSpec extends AnyFunSuite { val ipv6 = InetAddresses.forString("1080:0:0:0:8:800:200C:417A").asInstanceOf[Inet6Address] val bin = ipv6address.encode(ipv6).require val ipv62 = ipv6address.decode(bin).require.value - assert(ipv6 === ipv62) + assert(ipv6 == ipv62) } } @@ -168,31 +184,31 @@ class CommonCodecsSpec extends AnyFunSuite { val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address] val nodeaddr = IPv4(ipv4addr, 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"01 C0 A8 01 2A 10 87".toBitVector) + assert(bin == hex"01 C0 A8 01 2A 10 87".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray).asInstanceOf[Inet6Address] val nodeaddr = IPv6(ipv6addr, 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector) + assert(bin == hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val nodeaddr = Tor2("z4zif3fy7fe7bpg3", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector) + assert(bin == hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val nodeaddr = Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector) + assert(bin == hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } } @@ -213,8 +229,8 @@ class CommonCodecsSpec extends AnyFunSuite { expected_opt match { case Some(expected) => val decoded = bytes32.decode(encoded.bits).require.value - assert(decoded === expected) - assert(expected.bytes === bytes32.encode(decoded).require.bytes) + assert(decoded == expected) + assert(expected.bytes == bytes32.encode(decoded).require.bytes) case None => assert(bytes32.decode(encoded.bits).isFailure) } @@ -239,8 +255,8 @@ class CommonCodecsSpec extends AnyFunSuite { expected_opt match { case Some(expected) => val decoded = bytes64.decode(encoded.bits).require.value - assert(decoded === expected) - assert(expected.bytes === bytes64.encode(decoded).require.bytes) + assert(decoded == expected) + assert(expected.bytes == bytes64.encode(decoded).require.bytes) case None => assert(bytes64.decode(encoded.bits).isFailure) } @@ -269,17 +285,17 @@ class CommonCodecsSpec extends AnyFunSuite { { val alias = "IRATEMONK" val bin = c.encode(alias).require - assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) + assert(bin == BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) val alias2 = c.decode(bin).require.value - assert(alias === alias2) + assert(alias == alias2) } { val alias = "this-alias-is-exactly-32-B-long." val bin = c.encode(alias).require - assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) + assert(bin == BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) val alias2 = c.decode(bin).require.value - assert(alias === alias2) + assert(alias == alias2) } { @@ -298,17 +314,17 @@ class CommonCodecsSpec extends AnyFunSuite { for ((codec, expected, bin) <- testCases) { val macCodec = prependmac(codec, mac) val decoded = macCodec.decode(bin.toBitVector).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = macCodec.encode(expected).require.toByteVector - assert(encoded === bin) + assert(encoded == bin) } } test("backward compatibility on feerate codec") { val value = 123456 val feerate = FeeratePerKw(value sat) - assert(feeratePerKw.decode(uint32.encode(value).require).require === DecodeResult(feerate, BitVector.empty)) + assert(feeratePerKw.decode(uint32.encode(value).require).require == DecodeResult(feerate, BitVector.empty)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index 558b776c86..fa3c241427 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -16,11 +16,12 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64} +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -29,65 +30,65 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode a list of short channel ids") { { // encode/decode with encoding 'uncompressed' - val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { // encode/decode with encoding 'zlib' - val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { // encode/decode empty list with encoding 'uncompressed' val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty) val encoded = encodedShortChannelIdsCodec.encode(ids).require - assert(encoded.bytes === hex"00") + assert(encoded.bytes == hex"00") val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { // encode/decode empty list with encoding 'zlib' val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List.empty) val encoded = encodedShortChannelIdsCodec.encode(ids).require - assert(encoded.bytes === hex"00") // NB: empty list is always encoded with encoding type 'uncompressed' + assert(encoded.bytes == hex"00") // NB: empty list is always encoded with encoding type 'uncompressed' val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty)) + assert(decoded == EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty)) } } test("encode query_short_channel_ids (no optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode query_short_channel_ids (with optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)))) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode query_short_channel_ids (with optional data including unknown data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)) :: Nil, GenericTlv(UInt64(43), ByteVector.fromValidHex("deadbeef")) :: Nil @@ -96,7 +97,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode reply_channel_range (no optional data)") { @@ -104,12 +105,12 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("encode reply_channel_range (with optional timestamps)") { @@ -117,13 +118,13 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec)))), None) val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("encode reply_channel_range (with optional timestamps, checksums, and unknown data)") { @@ -131,7 +132,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( List( EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))), @@ -143,14 +144,14 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("compute checksums correctly (CL test #1)") { val update = ChannelUpdate( chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("76df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c80293282"), - shortChannelId = ShortChannelId("103x1x0"), + shortChannelId = ShortChannelId.fromCoordinates("103x1x0").success.value, timestamp = TimestampSecond(1565587763L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(144), @@ -170,7 +171,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val update = ChannelUpdate( chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("06737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c"), - shortChannelId = ShortChannelId("109x1x0"), + shortChannelId = ShortChannelId.fromCoordinates("109x1x0").success.value, timestamp = TimestampSecond(1565587765L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(48), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala index 466b68ef18..5a2ebade3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64} @@ -53,7 +53,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { msg => { val encoded = failureMessageCodec.encode(msg).require val decoded = failureMessageCodec.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } } @@ -74,8 +74,8 @@ class FailureMessageCodecsSpec extends AnyFunSuite { val decoded = failureMessageCodec.decode(bin.bits).require.value assert(decoded.isInstanceOf[FailureMessage]) assert(decoded.isInstanceOf[UnknownFailureMessage]) - assert(decoded.isInstanceOf[Node] === node) - assert(decoded.isInstanceOf[Perm] === perm) + assert(decoded.isInstanceOf[Node] == node) + assert(decoded.isInstanceOf[Perm] == perm) } } @@ -87,7 +87,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { ) for ((code, message) <- msgs) { - assert(message.code === code) + assert(message.code == code) } } @@ -100,10 +100,10 @@ class FailureMessageCodecsSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = codec.decode(bin.toBitVector).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = codec.encode(expected).require.toByteVector - assert(encoded === bin) + assert(encoded == bin) } } @@ -119,7 +119,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { ) for ((expected, bin) <- testCases) { - assert(codec.decode(bin.bits).require.value === expected) + assert(codec.decode(bin.bits).require.value == expected) } } @@ -151,10 +151,10 @@ class FailureMessageCodecsSpec extends AnyFunSuite { val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(14), 1000 msat, 1 msat, 1, None)) val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value - assert(u === ref) + assert(u == ref) val bin = ByteVector(failureMessageCodec.encode(u).require.toByteArray) - assert(bin === tmp_channel_failure_withtype) + assert(bin == tmp_channel_failure_withtype) val u2 = failureMessageCodec.decode(bin.toBitVector).require.value - assert(u2 === ref) + assert(u2 == ref) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index e7c1e6325b..064dfbf758 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -16,15 +16,20 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} +import com.google.common.base.Charsets +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong, ScriptWitness, Transaction} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelTypes +import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.protocol.ChannelTlv.{ChannelTypeTlv, UpfrontShutdownScriptTlv} import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ +import fr.acinq.eclair.wire.protocol.TxRbfTlv.SharedOutputContributionTlv import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult @@ -76,12 +81,12 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for (testCase <- testCases) { if (testCase.valid) { val init = initCodec.decode(testCase.encoded.bits).require.value - assert(init.features.toByteVector === testCase.rawFeatures) - assert(init.networks === testCase.networks) - assert(init.remoteAddress_opt === testCase.address) + assert(init.features.toByteVector == testCase.rawFeatures) + assert(init.networks == testCase.networks) + assert(init.remoteAddress_opt == testCase.address) val encoded = initCodec.encode(init).require - assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded)) - assert(initCodec.decode(encoded).require.value === init) + assert(encoded.bytes == testCase.reEncoded.getOrElse(testCase.encoded)) + assert(initCodec.decode(encoded).require.value == init) } else { assert(initCodec.decode(testCase.encoded.bits).isFailure) } @@ -97,8 +102,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) for ((warning, expected) <- testCases) { - assert(lightningMessageCodec.encode(warning).require.bytes === expected) - assert(lightningMessageCodec.decode(expected.bits).require.value === warning) + assert(lightningMessageCodec.encode(warning).require.bytes == expected) + assert(lightningMessageCodec.decode(expected.bits).require.value == warning) } } @@ -137,8 +142,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) refs.foreach { case ((bin, remainder), msg) => - assert(lightningMessageCodec.decode(bin.bits ++ remainder.bits).require === DecodeResult(msg, remainder.bits)) - assert(lightningMessageCodec.encode(msg).require === bin.bits) + assert(lightningMessageCodec.decode(bin.bits ++ remainder.bits).require == DecodeResult(msg, remainder.bits)) + assert(lightningMessageCodec.encode(msg).require == bin.bits) } } @@ -148,11 +153,48 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val node = nodeAnnouncementCodec.decode(bin).require.value val bin2 = nodeAnnouncementCodec.encode(node).require - assert(bin === bin2) + assert(bin == bin2) + } + + test("encode/decode interactive-tx messages") { + val channelId1 = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val channelId2 = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + // This is a random mainnet transaction. + val txBin1 = hex"020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" + val tx1 = Transaction.read(txBin1.toArray) + // This is random, longer mainnet transaction. + val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" + val tx2 = Transaction.read(txBin2.toArray) + val testCases = Seq( + TxAddInput(channelId1, UInt64(561), tx1, 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", + TxAddInput(channelId2, UInt64(0), tx2, 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", + TxAddInput(channelId1, UInt64(561), tx1, 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", + TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", + TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", + TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + TxComplete(channelId1, TlvStream(Nil, Seq(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", + TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 0002 00020002dead0002beef 0004 00000004010101010000000102", + TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 0000", + TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", + TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream(SharedOutputContributionTlv(5000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00021388", + TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxAckRbf(channelId2, TlvStream(SharedOutputContributionTlv(450000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000306ddd0", + TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000", + TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 000e 696e7465726e616c206572726f72", + ) + testCases.foreach { case (message, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require + assert(decoded.remainder.isEmpty) + assert(decoded.value == message) + val encoded = lightningMessageCodec.encode(message).require.bytes + assert(encoded == bin) + } } test("encode/decode open_channel") { - val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) + val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -179,14 +221,20 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))), - defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))), + defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (scid-alias) + defaultEncoded ++ hex"0000" ++ hex"0106 400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04000000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)))), + // empty upfront_shutdown_script + channel type (scid-alias + zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)))), ) for ((encoded, expected) <- testCases) { val decoded = openChannelCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = openChannelCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -208,6 +256,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode open_channel (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 01" + val testCases = Seq( + defaultOpen -> defaultEncoded, + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), + ) + testCases.foreach { case (open, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded == open) + val encoded = lightningMessageCodec.encode(open).require.bytes + assert(encoded == bin) + } + } + + test("decode open_channel with unknown channel flags (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncodedWithoutFlags = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"ff" -> ChannelFlags(true), + ) + testCases.foreach { case (bin, flags) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded == defaultOpen.copy(channelFlags = flags)) + } + } + test("encode/decode accept_channel") { val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6)) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. @@ -228,9 +306,24 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((encoded, expected) <- testCases) { val decoded = acceptChannelCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = acceptChannelCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) + } + } + + test("encode/decode accept_channel (dual funding)") { + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultAccept -> defaultEncoded, + defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey))) -> (defaultEncoded ++ hex"01021000"), + ) + testCases.foreach { case (accept, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded == accept) + val encoded = lightningMessageCodec.encode(accept).require.bytes + assert(encoded == bin) } } @@ -247,62 +340,58 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((encoded, expected) <- testCases) { val decoded = closingSignedCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = closingSignedCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } test("encode/decode all channel messages") { - val open = OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) - val accept = AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) - val funding_created = FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()) - val funding_signed = FundingSigned(randomBytes32(), randomBytes64()) - val funding_locked = FundingLocked(randomBytes32(), point(2)) - val update_fee = UpdateFee(randomBytes32(), FeeratePerKw(2 sat)) - val shutdown = Shutdown(randomBytes32(), bin(47, 0)) - val closing_signed = ClosingSigned(randomBytes32(), 2 sat, randomBytes64()) - val update_add_htlc = UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket) - val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)) - val update_fail_htlc = UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)) - val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111) - val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) - val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) - val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - val node_announcement = NodeAnnouncement(randomBytes64(), Features(bin(1, 2)), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) - val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) - val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) - val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) val unknownTlv = GenericTlv(UInt64(5), ByteVector.fromValidHex("deadbeef")) - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(100000), - 1500, - TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(100000), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), - TlvStream( - EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, - unknownTlv :: Nil) + val msgs = List( + OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private), + AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)), + FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()), + FundingSigned(randomBytes32(), randomBytes64()), + ChannelReady(randomBytes32(), point(2)), + ChannelReady(randomBytes32(), point(2), TlvStream(ChannelReadyTlv.ShortChannelIdTlv(Alias(123456)))), + UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), + Shutdown(randomBytes32(), bin(47, 0)), + ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), + UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket), + UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)), + UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)), + UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), + CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), + RevokeAndAck(randomBytes32(), scalar(0), point(1)), + ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, RealShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), + NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil), + ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None), + AnnouncementSignatures(randomBytes32(), RealShortChannelId(42), randomBytes64(), randomBytes64()), + GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500), + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), + QueryChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(100000), + 1500, + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)), + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(100000), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), + TlvStream( + EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, + unknownTlv :: Nil) + ), + Ping(100, bin(10, 1)), + Pong(bin(10, 1)), + ChannelReestablish(randomBytes32(), 242842L, 42L, randomKey(), randomKey().publicKey), + UnknownMessage(tag = 60000, data = ByteVector32.One.bytes), ) - val ping = Ping(100, bin(10, 1)) - val pong = Pong(bin(10, 1)) - val channel_reestablish = ChannelReestablish(randomBytes32(), 242842L, 42L, randomKey(), randomKey().publicKey) - - val unknown_message = UnknownMessage(tag = 60000, data = ByteVector32.One.bytes) - - val msgs: List[LightningMessage] = - open :: accept :: funding_created :: funding_signed :: funding_locked :: update_fee :: shutdown :: closing_signed :: - update_add_htlc :: update_fulfill_htlc :: update_fail_htlc :: update_fail_malformed_htlc :: commit_sig :: revoke_and_ack :: - channel_announcement :: node_announcement :: channel_update :: gossip_timestamp_filter :: query_short_channel_id :: query_channel_range :: reply_channel_range :: announcement_signatures :: - ping :: pong :: channel_reestablish :: unknown_message :: Nil msgs.foreach { msg => { val encoded = lightningMessageCodecWithFallback.encode(msg).require val decoded = lightningMessageCodecWithFallback.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } } @@ -314,19 +403,19 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val encoded1 = lightningMessageCodecWithFallback.encode(unknown).require val decoded1 = lightningMessageCodecWithFallback.decode(encoded1).require.value assert(lightningMessageCodec.decode(encoded1).isFailure) - assert(decoded1 === unknown) + assert(decoded1 == unknown) } test("non-reg encoding type") { val refs = Map( hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4010400010204" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3010c01789c6364620100000e0008" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) ) refs.forall { @@ -338,44 +427,44 @@ class LightningMessageCodecsSpec extends AnyFunSuite { case class TestItem(msg: Any, hex: String) test("test vectors for extended channel queries ") { - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream.empty) - val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(35000), - 100, - TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) - val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) - val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), - Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) - val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), - Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) - val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) - val query_short_channel_id_flags = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) - val query_short_channel_id_flags_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) val refs = Map( - query_channel_range -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", - query_channel_range_timestamps_checksums -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", - reply_channel_range -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", - reply_channel_range_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", - reply_channel_range_timestamps_checksums -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", - reply_channel_range_timestamps_checksums_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", - query_short_channel_id -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", - query_short_channel_id_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", - query_short_channel_id_flags -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", - query_short_channel_id_flags_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" + QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream.empty) -> + hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", + QueryChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(35000), + 100, + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) -> + hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(265462))), None, None) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(4564), RealShortChannelId(178622), RealShortChannelId(4564676))), TlvStream.empty) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12232), RealShortChannelId(15556), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(14200), RealShortChannelId(46645), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" ) val items = refs.map { case (obj, refbin) => val bin = lightningMessageCodec.encode(obj).require - assert(refbin.bits === bin) + assert(refbin.bits == bin) TestItem(obj, bin.toHex) } @@ -419,7 +508,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer + - new PaymentRequestSerializer + + new InvoiceSerializer + ShortTypeHints(List( classOf[QueryChannelRange], classOf[ReplyChannelRange], @@ -433,11 +522,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] - assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) + assert(update == ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) - assert(bin === bin2) + assert(bin == bin2) } test("non-regression on channel_update") { @@ -462,10 +551,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((bin, ref) <- bins) { val decoded = channelUpdateCodec.decode(bin.bits).require // not only must decoding succeed, it must also produce the same object - assert(Serialization.write(decoded.value)(JsonSerializers.formats) === ref) + assert(Serialization.write(decoded.value)(JsonSerializers.formats) == ref) assert(decoded.remainder.isEmpty) // encoding must produce the same buffer - assert(channelUpdateCodec.encode(decoded.value).require.bytes === bin) + assert(channelUpdateCodec.encode(decoded.value).require.bytes == bin) } } @@ -478,9 +567,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) for ((bin, ref) <- testCases) { val decoded = channelFlagsCodec.decode(bin).require - assert(decoded.value === ref) + assert(decoded.value == ref) assert(decoded.remainder.isEmpty) - assert(channelFlagsCodec.encode(ref).require === bin) + assert(channelFlagsCodec.encode(ref).require == bin) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala index 3c88f2adda..12768df26c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.MessageOnion._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs._ @@ -14,8 +14,8 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { test("simple relay payload") { val payload = RelayPayload(TlvStream(EncryptedData(hex"0a336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9"))) val serialized = hex"3704350a336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9" - assert(relayPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(relayPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(relayPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(relayPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("empty relay payload") { @@ -31,8 +31,8 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { test("simple final payload") { val payload = FinalPayload(TlvStream(EncryptedData(hex""))) val serialized = hex"020400" - assert(finalPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(finalPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("empty final payload") { @@ -47,34 +47,34 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { List(hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")) val payload = FinalPayload(TlvStream(ReplyPath(blindedRoute), EncryptedData(hex""))) val serialized = hex"f202ee02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661902c5ee5d5d559475814916957e167b8c257e06532ae6bfcbe4553e4549b9142ec7039dcddf597c0ea5bfe3c4de00630182d26c8f3cb588fa02c8cd19391a110f41a200330840ad82edc7378794e568deb3a836e3b9bc2e4a684412c34dbc5e50159ecf0b9c3844719f8656af9ff283e1eecb503f5e45b302aa42066bc9802597cac8f9f7193b8fd24b8671e3807e9c61dae8b330b695de780033d76f6388daa82694bcc63d43eaac1c5d189722cb84d0edb3b8b7dccb833c886eda7adb483f44498789f4139b2c12a0bfe8436a0400" - assert(finalPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(finalPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("onion packet can be any size") { { // small onion val onion = OnionRoutingPacket(1, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"0012345679abcdef", ByteVector32(hex"0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000")) val serialized = hex"004a 01 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 0012345679abcdef 0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // larger onion val onion = OnionRoutingPacket(2, hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", hex"0012345679abcdef012345679abcdef012345679abcdef012345679abcdef012345679abcdef", ByteVector32(hex"eeee0000111122223333444455556666777788889999aaaabbbbccccddddeeee")) val serialized = hex"0068 02 027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007 0012345679abcdef012345679abcdef012345679abcdef012345679abcdef012345679abcdef eeee0000111122223333444455556666777788889999aaaabbbbccccddddeeee" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion with trailing additional bytes val onion = OnionRoutingPacket(0, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"ffffffff", ByteVector32.Zeroes) val serialized = hex"0046 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 ffffffff 0000000000000000000000000000000000000000000000000000000000000000 0a01020000030400000000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized.dropRight(11)) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized.dropRight(11)) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion with empty payload val onion = OnionRoutingPacket(0, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"", ByteVector32.Zeroes) val serialized = hex"0042 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 0000000000000000000000000000000000000000000000000000000000000000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion length too big val serialized = hex"0048 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 ffffffff 0000000000000000000000000000000000000000000000000000000000000000" diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala new file mode 100644 index 0000000000..11916c64c0 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -0,0 +1,304 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} +import fr.acinq.eclair.wire.protocol.OfferCodecs.invoiceRequestTlvCodec +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey} +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.{ByteVector, HexStringSyntax} + +import scala.util.Success + +class OfferTypesSpec extends AnyFunSuite { + val nodeKey = PrivateKey(hex"85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d") + val nodeId = nodeKey.publicKey + + test("sign and check offer") { + val key = randomKey() + val offer = Offer(Some(100_000 msat), "test offer", key.publicKey, Features(VariableLengthOnion -> Mandatory), Block.LivenetGenesisBlock.hash) + assert(offer.signature.isEmpty) + val signedOffer = offer.sign(key) + assert(signedOffer.checkSignature()) + } + + test("invoice request is signed") { + val sellerKey = randomKey() + val offer = Offer(Some(100_000 msat), "test offer", sellerKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.checkSignature()) + } + + test("basic offer") { + val offer = Offer(TlvStream[OfferTlv]( + Description("basic offer"), + NodeId(nodeId))) + val encoded = "lno1pg9kyctnd93jqmmxvejhy83pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrj" + assert(Offer.decode(encoded).get == offer) + assert(offer.amount.isEmpty) + assert(offer.signature.isEmpty) + assert(offer.description == "basic offer") + assert(offer.nodeId == nodeId) + } + + test("basic signed offer") { + val signedOffer = Offer(TlvStream[OfferTlv]( + Description("basic signed offer"), + NodeId(nodeId))).sign(nodeKey) + val encoded = "lno1pgfxyctnd93jqumfvahx2epqdanxvetjrcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqr8k27dajwvrehuprj08ggamld6pgnj9ydp3whx6kz5hjaavh8rhfjzkhyxsjwakelepqxmx26aqhlaslxn8ljn4mtm2cx76xz72kxkc" + assert(Offer.decode(encoded).get == signedOffer) + assert(signedOffer.checkSignature()) + assert(signedOffer.amount.isEmpty) + assert(signedOffer.description == "basic signed offer") + assert(signedOffer.nodeId == nodeId) + } + + test("offer with amount and quantity") { + val offer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(50 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(1), + NodeId(nodeKey.publicKey))) + val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe" + assert(Offer.decode(encoded).get == offer) + assert(offer.amount.contains(50 msat)) + assert(offer.signature.isEmpty) + assert(offer.description == "offer with quantity") + assert(offer.nodeId == nodeId) + assert(offer.issuer.contains("alice@bigshop.com")) + assert(offer.quantityMin.contains(1)) + } + + test("signed offer with amount and quantity") { + val signedOffer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(50 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(1), + NodeId(nodeKey.publicKey))).sign(nodeKey) + val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqte5yn8m6mtk5racz9c0hgw6smxhp5t0ns77huttmy4632f6t2ns3jdwkxh9qy9f2eun2329gswcz38dn5f2us4us2r76zxvhkj9uecv" + assert(Offer.decode(encoded).get == signedOffer) + assert(signedOffer.checkSignature()) + assert(signedOffer.amount.contains(50 msat)) + assert(signedOffer.description == "offer with quantity") + assert(signedOffer.nodeId == nodeId) + assert(signedOffer.issuer.contains("alice@bigshop.com")) + assert(signedOffer.quantityMin.contains(1)) + } + + test("decode invalid offer") { + val testCases = Seq( + "lno1pgxx7enxv4e8xgrjda3kkgg", // missing node id + "lno1rcsdhss957tylk58rmly849jupnmzs52ydhxhl8fgz7994xkf2hnwhg", // missing description + ) + for (testCase <- testCases) { + assert(Offer.decode(testCase).isFailure) + } + } + + def signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest = { + val tlvs = removeSignature(request.records) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(tlvs, invoiceRequestTlvCodec), key) + val signedRequest = InvoiceRequest(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature)))) + assert(signedRequest.checkSignature()) + signedRequest + } + + test("check that invoice request matches offer") { + val offer = Offer(Some(2500 msat), "basic offer", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(3000 msat) case x => x })), payerKey) + assert(biggerAmount.isValidFor(offer)) + val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(2000 msat) case x => x })), payerKey) + assert(!lowerAmount.isValidFor(offer)) + val otherOfferId = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x })), payerKey) + assert(!otherOfferId.isValidFor(offer)) + val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Quantity(1)))), payerKey) + assert(!withQuantity.isValidFor(offer)) + } + + test("check that invoice request matches offer (with features)") { + val offer = Offer(Some(2500 msat), "offer with features", randomKey().publicKey, Features(VariableLengthOnion -> Optional), Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val withoutFeatures = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(withoutFeatures.isValidFor(offer)) + val otherFeatures = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory), payerKey, Block.LivenetGenesisBlock.hash) + assert(!otherFeatures.isValidFor(offer)) + } + + test("check that invoice request matches offer (without amount)") { + val offer = Offer(None, "offer without amount", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val withoutAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.filter { case Amount(_) => false case _ => true })), payerKey) + assert(!withoutAmount.isValidFor(offer)) + } + + test("check that invoice request matches offer (chain compatibility)") { + { + val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey)))) + val payerKey = randomKey() + val request = { + val tlvs: Seq[InvoiceRequestTlv] = Seq( + OfferId(offer.offerId), + Amount(100 msat), + PayerKey(payerKey.publicKey), + FeaturesTlv(Features.empty) + ) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + assert(request.isValidFor(offer)) + val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.LivenetGenesisBlock.hash)))), payerKey) + assert(withDefaultChain.isValidFor(offer)) + val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.TestnetGenesisBlock.hash)))), payerKey) + assert(!otherChain.isValidFor(offer)) + } + { + val (chain1, chain2) = (randomBytes32(), randomBytes32()) + val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey)))) + val payerKey = randomKey() + val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1) + assert(request1.isValidFor(offer)) + val request2 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain2) + assert(request2.isValidFor(offer)) + val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filter { case Chain(_) => false case _ => true })), payerKey) + assert(!noChain.isValidFor(offer)) + val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { case Chain(_) => Chain(Block.LivenetGenesisBlock.hash) case x => x })), payerKey) + assert(!otherChain.isValidFor(offer)) + } + } + + test("check that invoice request matches offer (multiple items)") { + val offer = Offer(TlvStream( + Amount(500 msat), + Description("offer for multiple items"), + NodeId(randomKey().publicKey), + QuantityMin(3), + QuantityMax(10), + )) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 1600 msat, 3, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.records.get[Quantity].nonEmpty) + assert(request.isValidFor(offer)) + val invalidAmount = InvoiceRequest(offer, 2400 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!invalidAmount.isValidFor(offer)) + val tooFewItems = InvoiceRequest(offer, 1000 msat, 2, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!tooFewItems.isValidFor(offer)) + val tooManyItems = InvoiceRequest(offer, 5500 msat, 11, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!tooManyItems.isValidFor(offer)) + } + + test("decode invoice request") { + val encoded = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypz8xu3xwsqpar9dd26lgrrvc7s63ljt0pgh6ag2utv5udez7n2mjzqzz47qcqczqgqzqqgzycsv2tmjgzc5l546aldq699wj9pdusvfred97l352p4aa862vqvzw5p8pdyjqctdyppxzardv9hrypx74klwluzqd0rqgeew2uhuagttuv6aqwklvm0xmlg52lfnagzw8ygt0wrtnv2tsx69m6tgug7njaw5ypa5fn369n9yzc87v02rqccj9h04dxf3nzc" + val Success(request) = InvoiceRequest.decode(encoded) + assert(request.amount == Some(5500 msat)) + assert(request.offerId == ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9")) + assert(request.quantity == 2) + assert(request.features == Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional)) + assert(request.records.get[Chain].nonEmpty) + assert(request.chain == Block.LivenetGenesisBlock.hash) + assert(request.payerKey == ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750")) + assert(request.payerInfo == Some(hex"deadbeef")) + assert(request.payerNote == Some("I am Batman")) + assert(request.encode() == encoded) + } + + test("decode invalid invoice request") { + val testCases = Seq( + // Missing offer id. + "lnr1pqpp8zqvqqnzqq7pw52tqj6pj2mar5cgkmnt9xe3tj40nxc3pp95xml2e8v432ny7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g", + // Missing payer key. + "lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqq7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g", + // Missing signature. + "lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqqycsq8st4zjcyksvjklgaxz9ku6efkv2u4tuekyggfdpkl6kfm9v25eq", + ) + for (testCase <- testCases) { + assert(InvoiceRequest.decode(testCase).isFailure) + } + } + + test("compute merkle tree root") { + import scodec.Codec + import scodec.codecs.list + + case class TestCase(tlvs: ByteVector, count: Int, expected: ByteVector32) + + val testCases = Seq( + // Official test vectors. + TestCase(hex"010203e8", 1, ByteVector32(hex"aa0aa0f694c85492ac459c1de9831a37682985f5e840ecc9b1e28eece7dc5236")), + TestCase(hex"010203e8 02080000010000020003", 2, ByteVector32(hex"013b756ed73554cbc4dd3d90f363cb7cba6d8a279465a21c464e582b173ff502")), + TestCase(hex"010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32(hex"016fcda3b6f9ca30b35936877ca591fa101365a761a1453cfd9436777d593656")), + TestCase(hex"0603555344 080203e8 0a0f313055534420657665727920646179 141072757374792e6f7a6c6162732e6f7267 1a020101 1e204b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605", 6, ByteVector32(hex"7cef68df49fd9222bed0138ca5603da06464b2f523ea773bc4edcb6bd07966e7")), + // Additional test vectors. + TestCase(hex"010100", 1, ByteVector32(hex"c8112b235945b06a11995bf69956a93ff0403c28de35bd33b4714da1b6239ebb")), + TestCase(hex"010100 020100", 2, ByteVector32(hex"8271d606bea3ef49e59d610585317edfc6c53d8d1afd763731919d9a7d70a7d9")), + TestCase(hex"010100 020100 030100", 3, ByteVector32(hex"c7eff290817749d87eede061d5335559e8211769e651a2ee5c5e7d2ddd655236")), + TestCase(hex"010100 020100 030100 040100", 4, ByteVector32(hex"57883ef2f1e8df4a23e6f0a2e3acda2ed0b11e00ef2d39fe1caa2d71d7273c37")), + TestCase(hex"010100 020100 030100 040100 050100", 5, ByteVector32(hex"85b74f254eced46c525a5369c52f86f249a41f6f6ccb3c918ffe4025ea22d8b6")), + TestCase(hex"010100 020100 030100 040100 050100 060100", 6, ByteVector32(hex"6cf27da8a67b7cb199dd1824017cb008bd22bf1d57273a8c4544c5408275dc2d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32(hex"2a038f022b51b1b969563679a22eb167ef603d5b2cb2d0dbe86dc4d2f48d8c6e")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32(hex"8ddbe97f6ed2e2a4a43e828e350f9cb6679b7d5f16837273cf0e6f7da342fa19")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32(hex"8050bed857ff7929a24251e3a517fc14f46fb0f02e6719cb9d53087f7f047f6d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32(hex"e22aa818e746ff9613e6cccc99ebce257d93c35736b168b6b478d6f3762f56ce")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32(hex"626e3159cec72534155ccf691a84ab68da89e6cd679a118c70a28fd1f1bb10cc")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32(hex"f659da1c839d99a2c6b104d179ee44ffe3a9eaa55831de3c612c8c293c27401b")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32(hex"c165756a94718d19e66ff7b581347699009a9e17805e16cb6ba94c034c7dc757")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32(hex"573b85bbceacbf1b189412858ac6573e923bbf0c9cfdc37d37757996f6086208")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32(hex"84a3088fe74b82ee82a9415d48fdfad8dc6a286fec8e6fcdcefcf0bc02f3256e")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32(hex"a686f116fce33e43fa875fec2253c71694a0324e1ce7640ed1070b0cc3a14cc1")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32(hex"fbee87d6726c8b67a8d2e2bff92b13d0b1d9188f9d42af2d3afefceaafa6f3e5")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32(hex"5004f619c426b01e57c480a84d5dcdc3a70b4bf575ec573be60c3a75ed978b72")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32(hex"6f0a5e59f1fa5dc6c12ed3bbe0eb91c818b22a8d011d5a2160462c59e6158a58")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", 20, ByteVector32(hex"e43f00c262e4578c5ed4413ab340e79cb8b258241b7c52550b7307f7b9c4d645")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", 21, ByteVector32(hex"e46776637883bae1a62cbfb621c310c13e6c522092954e08d74c08328d53f035")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", 22, ByteVector32(hex"813ebe9f07638005abbe270f11ae2749a5b9b0c5cf89a305598303a38f5f2da5")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", 23, ByteVector32(hex"fdd7b779192dcbadb5695303e2bcee0fc175428278bdbfa4b4445251df6c9450")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", 24, ByteVector32(hex"33c92b7820742d094548328ee3bfdf29bf3fe1f971171dcd2a6da0f185dceddb")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", 25, ByteVector32(hex"888da09f5ba1b8e431b3ab1db62fca94c0cbbec6b145012d9308d20f68571ff2")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", 26, ByteVector32(hex"a87cdc040109b855d81f13af4a6f57cdb7e31252eeb83bc03518fdd6dd81ec18")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", 27, ByteVector32(hex"9829715a0d8cbb5c080de53704f274aa4da3590e8338d57ce99ab491d7a44e76")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", 28, ByteVector32(hex"ce8bac7c3d10b528d59d5f391bf36bb6acd65b4bb3cbd0a769488e3b451b2c26")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", 29, ByteVector32(hex"88d29ac3e4ae8761058af4b1baaa873ec4f76822166f8dfc2888bcbb51212130")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", 30, ByteVector32(hex"b013259fe32c6eaf88d2b3b2d01350e5505bcc0fcdcdc7c360e5644fe827424d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", 31, ByteVector32(hex"1c60489269d312c2ea94c637936e38a968d2900cab6c5544db091aa8b3bb5176")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", 32, ByteVector32(hex"e0d88bd7685ffd55e0de4e45e190e7e6bf1ecc0a7d1a32fbdaa6b1b27e8bc37b")), + ) + testCases.foreach { + case TestCase(tlvStream, tlvCount, expectedRoot) => + val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList) + val tlvs = genericTlvStream.decode(tlvStream.bits).require.value + assert(tlvs.records.size == tlvCount) + val root = OfferTypes.rootHash(tlvs, genericTlvStream) + assert(root == expectedRoot) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 98cbb4b660..499ff7e057 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -16,10 +16,10 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64.Conversions._ -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.PaymentOnion._ @@ -40,10 +40,10 @@ class PaymentOnionSpec extends AnyFunSuite { val expected = OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) val decoded = paymentOnionPacketCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = paymentOnionPacketCodec.encode(decoded).require - assert(encoded.toByteVector === bin) + assert(encoded.toByteVector == bin) } test("encode/decode fixed-size (legacy) relay per-hop payload") { @@ -55,10 +55,10 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = channelRelayPerHopPayloadCodec.encode(expected).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -75,7 +75,7 @@ class PaymentOnionSpec extends AnyFunSuite { ) for ((payloadLength, bin) <- testCases) { - assert(payloadLengthDecoder.decode(bin.bits).require.value === payloadLength) + assert(payloadLengthDecoder.decode(bin.bits).require.value == payloadLength) } } @@ -88,13 +88,13 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === ChannelRelayTlvPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingChannelId === ShortChannelId(1105)) + assert(decoded == ChannelRelayTlvPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingChannelId == ShortChannelId(1105)) val encoded = channelRelayPerHopPayloadCodec.encode(ChannelRelayTlvPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -104,17 +104,17 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"2e 02020231 04012a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === NodeRelayPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.totalAmount === 561.msat) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingNodeId === nodeId) - assert(decoded.paymentSecret === None) - assert(decoded.invoiceFeatures === None) - assert(decoded.invoiceRoutingInfo === None) + assert(decoded == NodeRelayPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.totalAmount == 561.msat) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingNodeId == nodeId) + assert(decoded.paymentSecret == None) + assert(decoded.invoiceFeatures == None) + assert(decoded.invoiceRoutingInfo == None) val encoded = nodeRelayPerHopPayloadCodec.encode(NodeRelayPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } test("encode/decode variable-length (tlv) node relay to legacy per-hop payload") { @@ -129,17 +129,17 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"fa 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 fe00010231010a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe000102339b01036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e200000000000000010000000a00000064009002025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce148600000000000000020000001400000096000c02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee00000000000000030000001e000000c80018" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === NodeRelayPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.totalAmount === 1105.msat) - assert(decoded.paymentSecret === Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingNodeId === nodeId) - assert(decoded.invoiceFeatures === Some(features)) - assert(decoded.invoiceRoutingInfo === Some(routingHints)) + assert(decoded == NodeRelayPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.totalAmount == 1105.msat) + assert(decoded.paymentSecret == Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingNodeId == nodeId) + assert(decoded.invoiceFeatures == Some(features)) + assert(decoded.invoiceRoutingInfo == Some(routingHints)) val encoded = nodeRelayPerHopPayloadCodec.encode(NodeRelayPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } test("encode/decode variable-length (tlv) final per-hop payload") { @@ -157,12 +157,12 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = finalPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === FinalTlvPayload(expected)) - assert(decoded.amount === 561.msat) - assert(decoded.expiry === CltvExpiry(42)) + assert(decoded == FinalTlvPayload(expected)) + assert(decoded.amount == 561.msat) + assert(decoded.expiry == CltvExpiry(42)) val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -171,22 +171,22 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"53 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 ff0000000143c7a0402016c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828" val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(tlvs)).require.bytes - assert(encoded === bin) + assert(encoded == bin) assert(finalPerHopPayloadCodec.decode(bin.bits).require.value == FinalTlvPayload(tlvs)) } test("decode multi-part final per-hop payload") { val multiPart = finalPerHopPayloadCodec.decode(hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451".bits).require.value - assert(multiPart.amount === 561.msat) - assert(multiPart.expiry === CltvExpiry(42)) - assert(multiPart.totalAmount === 1105.msat) - assert(multiPart.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPart.amount == 561.msat) + assert(multiPart.expiry == CltvExpiry(42)) + assert(multiPart.totalAmount == 1105.msat) + assert(multiPart.paymentSecret contains ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) val multiPartNoTotalAmount = finalPerHopPayloadCodec.decode(hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".bits).require.value - assert(multiPartNoTotalAmount.amount === 561.msat) - assert(multiPartNoTotalAmount.expiry === CltvExpiry(42)) - assert(multiPartNoTotalAmount.totalAmount === 561.msat) - assert(multiPartNoTotalAmount.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPartNoTotalAmount.amount == 561.msat) + assert(multiPartNoTotalAmount.expiry == CltvExpiry(42)) + assert(multiPartNoTotalAmount.totalAmount == 561.msat) + assert(multiPartNoTotalAmount.paymentSecret contains ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) } test("decode variable-length (tlv) relay per-hop payload missing information") { @@ -200,7 +200,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } @@ -215,7 +215,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } @@ -230,7 +230,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = finalPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala index 67eaea0dd7..3e7f63fe60 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} @@ -27,9 +27,9 @@ class RouteBlindingSpec extends AnyFunSuiteLike { for ((encoded, data) <- payloads) { val decoded = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.decode(encoded.bits).require.value - assert(decoded === data) + assert(decoded == data) val reEncoded = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.encode(data).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -51,7 +51,7 @@ class RouteBlindingSpec extends AnyFunSuiteLike { val Success((decryptedPayload2, blinding3)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(2), blinding2, blindedRoute.encryptedPayloads(2)) val Success((decryptedPayload3, blinding4)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(3), blinding3, blindedRoute.encryptedPayloads(3)) val Success((decryptedPayload4, _)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(4), blinding4, blindedRoute.encryptedPayloads(4)) - assert(Seq(decryptedPayload0, decryptedPayload1, decryptedPayload2, decryptedPayload3, decryptedPayload4) === payloads.map(_._1)) + assert(Seq(decryptedPayload0, decryptedPayload1, decryptedPayload2, decryptedPayload3, decryptedPayload4) == payloads.map(_._1)) } test("decode invalid encrypted route blinding data") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala index df4b75264a..d13a7b5374 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, shortchannelid, uint64, varint} import fr.acinq.eclair.wire.protocol.TlvCodecs._ @@ -48,13 +48,13 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu16.decode(bin.bits).require.value val decoded1 = ltu16.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu16.encode(expected).require.bytes val encoded1 = ltu16.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } @@ -77,13 +77,13 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu32.decode(bin.bits).require.value val decoded1 = ltu32.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu32.encode(expected).require.bytes val encoded1 = ltu32.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } @@ -118,32 +118,32 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu64.decode(bin.bits).require.value val decoded1 = ltu64.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu64.encode(expected).require.bytes val encoded1 = ltu64.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } test("encode/decode truncated uint64 overflow") { - assert(tu64overflow.encode(Long.MaxValue).require.toByteVector === hex"7fffffffffffffff") - assert(tu64overflow.decode(hex"7fffffffffffffff".bits).require.value === Long.MaxValue) + assert(tu64overflow.encode(Long.MaxValue).require.toByteVector == hex"7fffffffffffffff") + assert(tu64overflow.decode(hex"7fffffffffffffff".bits).require.value == Long.MaxValue) - assert(tu64overflow.encode(42L).require.toByteVector === hex"2a") - assert(tu64overflow.decode(hex"2a".bits).require.value === 42L) + assert(tu64overflow.encode(42L).require.toByteVector == hex"2a") + assert(tu64overflow.decode(hex"2a".bits).require.value == 42L) assert(tu64overflow.encode(-1L).isFailure) assert(tu64overflow.decode(hex"8000000000000000".bits).isFailure) } test("decode length-prefixed truncated uint64 ignores trailing bytes") { - assert(ltu64.decode(hex"00 1234".bits).require.value === UInt64(0)) - assert(ltu64.decode(hex"01 2a ff".bits).require.value === UInt64(42)) - assert(ltu64.decode(hex"02 0451 1234".bits).require.value === UInt64(1105)) - assert(ltu64.decode(hex"03 010000 0000".bits).require.value === UInt64(65536)) + assert(ltu64.decode(hex"00 1234".bits).require.value == UInt64(0)) + assert(ltu64.decode(hex"01 2a ff".bits).require.value == UInt64(42)) + assert(ltu64.decode(hex"02 0451 1234".bits).require.value == UInt64(1105)) + assert(ltu64.decode(hex"03 010000 0000".bits).require.value == UInt64(65536)) } test("decode invalid truncated integers") { @@ -200,9 +200,9 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, expected) <- testCases) { val decoded = testTlvStreamCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = testTlvStreamCodec.encode(expected).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -275,7 +275,7 @@ class TlvCodecsSpec extends AnyFunSuite { ) for (testCase <- testCases) { - assert(lengthPrefixedTestTlvStreamCodec.encode(lengthPrefixedTestTlvStreamCodec.decode(testCase.bits).require.value).require.bytes === testCase) + assert(lengthPrefixedTestTlvStreamCodec.encode(lengthPrefixedTestTlvStreamCodec.decode(testCase.bits).require.value).require.bytes == testCase) } } @@ -302,8 +302,8 @@ class TlvCodecsSpec extends AnyFunSuite { test("encode unordered tlv stream (codec should sort appropriately)") { val stream = TlvStream[TestTlv](Seq(TestType254(42), TestType1(42)), Seq(GenericTlv(13, hex"2a"), GenericTlv(11, hex"2b"))) - assert(testTlvStreamCodec.encode(stream).require.toByteVector === hex"01012a 0b012b 0d012a fd00fe02002a") - assert(lengthPrefixedTestTlvStreamCodec.encode(stream).require.toByteVector === hex"0f 01012a 0b012b 0d012a fd00fe02002a") + assert(testTlvStreamCodec.encode(stream).require.toByteVector == hex"01012a 0b012b 0d012a fd00fe02002a") + assert(lengthPrefixedTestTlvStreamCodec.encode(stream).require.toByteVector == hex"0f 01012a 0b012b 0d012a fd00fe02002a") } test("encode/decode custom even tlv records") { @@ -334,9 +334,9 @@ class TlvCodecsSpec extends AnyFunSuite { test("get optional TLV field") { val stream = TlvStream[TestTlv](Seq(TestType254(42), TestType1(42)), Seq(GenericTlv(13, hex"2a"), GenericTlv(11, hex"2b"))) - assert(stream.get[TestType254] === Some(TestType254(42))) - assert(stream.get[TestType1] === Some(TestType1(42))) - assert(stream.get[TestType2] === None) + assert(stream.get[TestType254] == Some(TestType254(42))) + assert(stream.get[TestType1] == Some(TestType1(42))) + assert(stream.get[TestType2] == None) } } diff --git a/eclair-front/modules/awseb/run.sh b/eclair-front/modules/awseb/run.sh index e3d7d8ecc1..9c6f0401a8 100644 --- a/eclair-front/modules/awseb/run.sh +++ b/eclair-front/modules/awseb/run.sh @@ -1,4 +1,6 @@ -export LOCAL_IP=$(curl -s 169.254.169.254/latest/meta-data/local-ipv4) +# see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html +TOKEN_IMDSV2=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +export LOCAL_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN_IMDSV2" http://169.254.169.254/latest/meta-data/local-ipv4) export HOSTNAME=$(hostname) # make the eclair home directory diff --git a/eclair-front/pom.xml b/eclair-front/pom.xml index 5f92d0f0c2..8166c379c8 100644 --- a/eclair-front/pom.xml +++ b/eclair-front/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.1-SNAPSHOT eclair-front_2.13 diff --git a/eclair-front/src/main/resources/logback.xml b/eclair-front/src/main/resources/logback.xml index f7667a5d39..bb7f4348ce 100644 --- a/eclair-front/src/main/resources/logback.xml +++ b/eclair-front/src/main/resources/logback.xml @@ -21,7 +21,7 @@ System.out false - %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n @@ -35,7 +35,7 @@ 5GB - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n @@ -57,7 +57,7 @@ - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala index 23736eddd2..90d8e57b31 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala @@ -24,7 +24,7 @@ import akka.pattern.ask import akka.util.Timeout import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.LocalNodeKeyManager import fr.acinq.eclair.io.Switchboard.{GetRouterPeerConf, RouterPeerConf} @@ -85,6 +85,8 @@ class FrontSetup(datadir: File)(implicit system: ActorSystem) extends Logging { config.getString("server.binding-ip"), config.getInt("server.port")) + val socks5ProxyParams_opt = NodeParams.parseSocks5ProxyParams(config) + def bootstrap: Future[Unit] = { val frontJoinedCluster = Promise[Done]() @@ -112,7 +114,7 @@ class FrontSetup(datadir: File)(implicit system: ActorSystem) extends Logging { frontRouter = system.actorOf(SimpleSupervisor.props(FrontRouter.props(routerConf, remoteRouter, Some(frontRouterInitialized)), "front-router", SupervisorStrategy.Resume)) _ <- frontRouterInitialized.future - clientSpawner = system.actorOf(Props(new ClientSpawner(keyPair, None, peerConnectionConf, remoteSwitchboard, frontRouter)), name = "client-spawner") + clientSpawner = system.actorOf(Props(new ClientSpawner(keyPair, socks5ProxyParams_opt, peerConnectionConf, remoteSwitchboard, frontRouter)), name = "client-spawner") server = system.actorOf(SimpleSupervisor.props(Server.props(keyPair, peerConnectionConf, remoteSwitchboard, frontRouter, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) } yield () diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala index a3a7ea1d5b..ed1f868cb3 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala @@ -20,9 +20,10 @@ import akka.Done import akka.actor.{ActorRef, Props} import akka.event.Logging.MDC import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Router._ @@ -110,7 +111,7 @@ class FrontRouter(routerConf: RouterConf, remoteRouter: ActorRef, initialized: O origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d - case u: ChannelUpdate if d.channels.contains(u.shortChannelId) && d.channels(u.shortChannelId).getChannelUpdateSameSideAs(u).contains(u) => + case u: ChannelUpdate if d.channels.get(RealShortChannelId(u.shortChannelId.toLong)).exists(_.getChannelUpdateSameSideAs(u).contains(u)) => origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d @@ -213,7 +214,7 @@ object FrontRouter { // @formatter:on case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], processing: Map[AnnouncementMessage, Set[RemoteGossip]], accepted: Map[AnnouncementMessage, Set[RemoteGossip]], rebroadcast: Rebroadcast) @@ -263,7 +264,7 @@ object FrontRouter { case ChannelsDiscovered(channels) => log.debug("adding {} channels", channels.size) - val channels1 = channels.foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { + val channels1 = channels.foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { case (channels, sc) => channels + (sc.ann.shortChannelId -> PublicChannel(sc.ann, ByteVector32.Zeroes, sc.capacity, sc.u1_opt, sc.u2_opt, None)) } val d1 = d.copy(channels = d.channels ++ channels1) @@ -280,7 +281,7 @@ object FrontRouter { case ChannelUpdatesReceived(updates) => log.debug("adding/updating {} channel_updates", updates.size) val channels1 = updates.foldLeft(d.channels) { - case (channels, u) => channels.get(u.shortChannelId) match { + case (channels, u) => channels.get(RealShortChannelId(u.shortChannelId.toLong)) match { case Some(c) => channels + (c.ann.shortChannelId -> c.updateChannelUpdateSameSideAs(u)) case None => channels } @@ -309,7 +310,7 @@ object FrontRouter { case n: NodeAnnouncement => d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)) case c: ChannelAnnouncement => d.rebroadcast.copy(channels = d.rebroadcast.channels + (c -> origins)) case u: ChannelUpdate => - if (d.channels.contains(u.shortChannelId)) { + if (d.channels.contains(RealShortChannelId(u.shortChannelId.toLong))) { d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)) } else { d.rebroadcast // private channel, we don't rebroadcast the channel_update diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index 51b2c6a7c5..0501dfcf67 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -18,10 +18,10 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.actor.typed.scaladsl.adapter.actorRefAdapter -import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, SatoshiLong, Transaction, TxOut} +import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} @@ -32,9 +32,6 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.Color import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits._ - -import scala.concurrent.duration._ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike { @@ -92,7 +89,7 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike pipe1.expectMsg(PeerRoutingMessage(front1, origin1a.nodeId, chan_ab)) pipe1.send(router, PeerRoutingMessage(pipe1.ref, origin1a.nodeId, chan_ab)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) peerConnection1b.send(front1, PeerRoutingMessage(peerConnection1b.ref, origin1b.nodeId, chan_ab)) pipe1.expectNoMessage() @@ -131,12 +128,14 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection1b.expectMsg(GossipDecision.Accepted(chan_ab)) peerConnection2a.expectMsg(GossipDecision.Accepted(chan_ab)) - // we have to wait 2 times the broadcast interval because there is an additional per-peer delay - val maxBroadcastDelay = 2 * nodeParams.routerConf.routerBroadcastInterval + 1.second - peerConnection1a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) - peerConnection1b.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) - peerConnection2a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map.empty, nodes = Map.empty)) - peerConnection3a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map.empty, nodes = Map.empty)) + // manual rebroadcast + front1 ! Router.TickBroadcast + peerConnection1a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) + peerConnection1b.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) + front2 ! Router.TickBroadcast + peerConnection2a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map.empty, nodes = Map.empty)) + front3 ! Router.TickBroadcast + peerConnection3a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map.empty, nodes = Map.empty)) } test("aggregate gossip") { @@ -149,9 +148,18 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike val system2 = ActorSystem("front-system-2") val system3 = ActorSystem("front-system-3") - val front1 = system1.actorOf(FrontRouter.props(nodeParams.routerConf, router)) - val front2 = system2.actorOf(FrontRouter.props(nodeParams.routerConf, router)) - val front3 = system3.actorOf(FrontRouter.props(nodeParams.routerConf, router)) + val front1 = { + implicit val system: ActorSystem = system1 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } + val front2 = { + implicit val system: ActorSystem = system2 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } + val front3 = { + implicit val system: ActorSystem = system3 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } val peerConnection1a = TestProbe("peerconn-1a") val peerConnection1b = TestProbe("peerconn-1b") @@ -169,7 +177,7 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike val origin3a = RemoteGossip(peerConnection3a.ref, randomKey().publicKey) peerConnection1a.send(front1, PeerRoutingMessage(peerConnection1a.ref, origin1a.nodeId, chan_ab)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) peerConnection1b.send(front1, PeerRoutingMessage(peerConnection1b.ref, origin1b.nodeId, chan_ab)) peerConnection2a.send(front2, PeerRoutingMessage(peerConnection2a.ref, origin2a.nodeId, chan_ab)) @@ -182,7 +190,6 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection3a.expectMsg(TransportHandler.ReadAck(channelUpdate_bc)) peerConnection3a.expectMsg(GossipDecision.NoRelatedChannel(channelUpdate_bc)) - watcher.send(router, ValidateResult(chan_ab, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_b)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) peerConnection1a.expectMsg(TransportHandler.ReadAck(chan_ab)) @@ -207,12 +214,18 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection3a.expectMsg(TransportHandler.ReadAck(ann_b)) peerConnection3a.expectMsg(GossipDecision.Accepted(ann_b)) - // we have to wait 2 times the broadcast interval because there is an additional per-peer delay - val maxBroadcastDelay = 2 * nodeParams.routerConf.routerBroadcastInterval + 1.second - peerConnection1a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection1b.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection2a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection3a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set(origin3a)), nodes = Map(ann_a -> Set(origin3a), ann_b -> Set(origin3a)))) + awaitCond(front1.stateData.nodes.size == 2) + awaitCond(front2.stateData.nodes.size == 2) + awaitCond(front3.stateData.nodes.size == 2) + + // manual rebroadcast + front1 ! Router.TickBroadcast + peerConnection1a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + peerConnection1b.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + front2 ! Router.TickBroadcast + peerConnection2a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + front3 ! Router.TickBroadcast + peerConnection3a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set(origin3a)), nodes = Map(ann_a -> Set(origin3a), ann_b -> Set(origin3a)))) } test("do not forward duplicate gossip") { @@ -320,19 +333,19 @@ object FrontRouterSpec { val (priv_funding_a, priv_funding_b, priv_funding_c, priv_funding_d, priv_funding_e, priv_funding_f) = (randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey()) val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey) - val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(hex"0200")) - val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features(hex"")) - val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(hex"0200")) - val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features(hex"00")) - val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features(hex"00")) - val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features(hex"00")) + val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional)) + val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty) + val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional)) + val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty) + val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) + val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) - val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val channelId_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val channelId_ef = ShortChannelId(BlockHeight(420000), 4, 0) + val channelId_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val channelId_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val channelId_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val channelId_ef = RealShortChannelId(BlockHeight(420000), 4, 0) - def channelAnnouncement(shortChannelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index a46b4adbd5..5f6fe8f97b 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.1-SNAPSHOT eclair-node_2.13 @@ -101,7 +101,7 @@ de.heikoseeberger akka-http-json4s_${scala.version.short} - 1.37.0 + 1.39.2 @@ -135,7 +135,7 @@ org.mockito mockito-scala-scalatest_${scala.version.short} - 1.5.9 + 1.17.5 test diff --git a/eclair-node/src/main/resources/application.conf b/eclair-node/src/main/resources/application.conf index 930d863bb0..eaa336ffde 100644 --- a/eclair-node/src/main/resources/application.conf +++ b/eclair-node/src/main/resources/application.conf @@ -43,6 +43,15 @@ kamon.instrumentation.akka { } } } +// See documentation here: https://kamon.io/docs/latest/reporters/prometheus/ +kamon.prometheus { + // If you want to expose a scraping endpoint for Prometheus metrics, set this to true. + start-embedded-http-server = no + embedded-server { + hostname = 0.0.0.0 + port = 9095 + } +} kanela.modules { jdbc { diff --git a/eclair-node/src/main/resources/logback.xml b/eclair-node/src/main/resources/logback.xml index 24b37b6544..4b25179b74 100644 --- a/eclair-node/src/main/resources/logback.xml +++ b/eclair-node/src/main/resources/logback.xml @@ -21,7 +21,7 @@ System.out false - %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n @@ -35,7 +35,7 @@ 5GB - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n @@ -57,7 +57,7 @@ - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index 4af2e3c6f8..37062f2eb0 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -21,13 +21,14 @@ import akka.http.scaladsl.marshalling.ToResponseMarshaller import akka.http.scaladsl.model.StatusCodes.NotFound import akka.http.scaladsl.model.{ContentTypes, HttpResponse} import akka.http.scaladsl.server.{Directive1, Directives, MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ -import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong} +import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scala.concurrent.Future import scala.util.{Failure, Success} @@ -45,11 +46,12 @@ trait ExtraDirectives extends Directives { val fromFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "from".as[TimestampSecond](timestampSecondUnmarshaller).?(TimestampSecond.min) val toFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "to".as[TimestampSecond](timestampSecondUnmarshaller).?(TimestampSecond.max) val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi] - val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] + val invoiceFormParam: NameReceptacle[Bolt11Invoice] = "invoice".as[Bolt11Invoice] val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi] + val offerFormParam: NameUnmarshallerReceptacle[Offer] = "offer".as[Offer](offerUnmarshaller) // custom directive to fail with HTTP 404 (and JSON response) if the element was not found def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 4ac5155d04..f7a04b0263 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} import akka.util.Timeout -import fr.acinq.bitcoin.{Satoshi, Script} +import fr.acinq.bitcoin.scalacompat.{Satoshi, Script} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives @@ -32,22 +32,31 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} + val supportedChannelTypes = Set( + ChannelTypes.Standard, + ChannelTypes.StaticRemoteKey, + ChannelTypes.AnchorOutputs, + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true) + ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api + val open: Route = postRequest("open") { implicit t => - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, channelFlags, openTimeout_opt) => - val (channelTypeOk, channelType_opt) = channelType match { - case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) - case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) - case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs)) - case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) - case Some(_) => (false, None) + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { + (nodeId, fundingSatoshis, pushMsat, channelTypeName_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => + val (channelTypeOk, channelType_opt) = channelTypeName_opt match { + case Some(channelTypeName) => supportedChannelTypes.get(channelTypeName) match { + case Some(channelType) => (true, Some(channelType)) + case None => (false, None) // invalid channel type name + } case None => (true, None) } if (!channelTypeOk) { - reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString}")) + reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be one of ${supportedChannelTypes.keys.mkString(",")}")) } else { complete { - eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, channelFlags, openTimeout_opt) + eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) } } } @@ -106,6 +115,10 @@ trait Channel { } } - val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats + val channelBalances: Route = postRequest("channelbalances") { implicit t => + complete(eclairApi.channelBalances()) + } + + val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala index 94dd48e31c..884a3fa3ef 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala index 975a882608..1f9db32194 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala index da5cf1a23c..a3a835015b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import com.google.common.net.HostAndPort import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.io.NodeURI +import fr.acinq.eclair.wire.protocol.NodeAddress trait Node { this: Service with EclairDirectives => @@ -38,7 +38,7 @@ trait Node { } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => complete { eclairApi.connect( - Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))) + Left(NodeURI(nodeId, NodeAddress.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)).get)) ) } } ~ formFields(nodeIdFormParam) { nodeId => @@ -62,5 +62,9 @@ trait Node { } } - val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit + val stop: Route = postRequest("stop") { implicit t => + complete(eclairApi.stop()) + } + + val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit ~ stop } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala index ee6fad9487..7074b49bfd 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index 48cc596681..e8241d7372 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -17,11 +17,10 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.{EclairDirectives, RouteFormat} import fr.acinq.eclair.api.serde.FormParamExtractors._ -import fr.acinq.eclair.payment.PaymentRequest import scala.concurrent.ExecutionContext @@ -34,10 +33,10 @@ trait PathFinding { val findRoute: Route = postRequest("findroute") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + case (invoice, None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) if invoice.amount_opt.nonEmpty => + complete(eclairApi.findRoute(invoice.nodeId, invoice.amount_opt.get, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case _ => reject(MalformedFormFieldRejection( "invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'" )) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index 67f341ec71..74b5cd4233 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -17,12 +17,12 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors.{pubkeyListUnmarshaller, _} -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.router.Router.{PredefinedChannelRoute, PredefinedNodeRoute} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, randomBytes32} @@ -39,7 +39,7 @@ trait Payment { val payInvoice: Route = postRequest("payinvoice") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "blocking".as[Boolean].?, "pathFindingExperimentName".?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => + case (invoice@Bolt11Invoice(_, Some(amount), _, nodeId, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => blocking_opt match { case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) case _ => complete(eclairApi.send(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) @@ -55,15 +55,15 @@ trait Payment { val sendToRoute: Route = postRequest("sendtoroute") { implicit t => withRoute { hops => - formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, + formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { - (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { + (amountMsat, recipientAmountMsat_opt, invoice, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { val route = hops match { case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) case Right(nodeIds) => PredefinedNodeRoute(nodeIds) } complete(eclairApi.sendToRoute( - amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, + amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil) )) } @@ -94,6 +94,13 @@ trait Payment { } } - val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo + val payOffer: Route = postRequest("payoffer") { implicit t => + formFields(offerFormParam, amountMsatFormParam, "quantity".as[Long].?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?) { + case (offer, amountMsat, quantity_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt) => + complete(eclairApi.payOffer(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) + } + } + + val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo ~ payOffer } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 7606a92277..40c8b98156 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -18,15 +18,16 @@ package fr.acinq.eclair.api.serde import akka.http.scaladsl.unmarshalling.Unmarshaller import akka.util.Timeout -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.api.directives.RouteFormat import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector @@ -44,11 +45,11 @@ object FormParamExtractors { implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin)) - implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest => PaymentRequest.read(rawRequest) } + implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest).get } - implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) } + implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId.fromCoordinates(str).get } - implicit val shortChannelIdsUnmarshaller: Unmarshaller[String, List[ShortChannelId]] = listUnmarshaller(str => ShortChannelId(str)) + implicit val shortChannelIdsUnmarshaller: Unmarshaller[String, List[ShortChannelId]] = listUnmarshaller(str => ShortChannelId.fromCoordinates(str).get) implicit val javaUUIDUnmarshaller: Unmarshaller[String, UUID] = Unmarshaller.strict { str => UUID.fromString(str) } @@ -74,6 +75,8 @@ object FormParamExtractors { blindedRouteCodec.decode(ByteVector.fromValidHex(str).bits).require.value } + val offerUnmarshaller: Unmarshaller[String, Offer] = Unmarshaller.strict { Offer.decode(_).get } + private def listUnmarshaller[T](unmarshal: String => T): Unmarshaller[String, List[T]] = Unmarshaller.strict { str => Try(serialization.read[List[String]](str).map(unmarshal)) .recoverWith(_ => Try(str.split(",").toList.map(unmarshal))) diff --git a/eclair-node/src/test/resources/api/channelbalances b/eclair-node/src/test/resources/api/channelbalances new file mode 100644 index 0000000000..bbfc7619b3 --- /dev/null +++ b/eclair-node/src/test/resources/api/channelbalances @@ -0,0 +1 @@ +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"temporary","realScid":"0x0x1"},"localAlias":"0x2"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x3"},"localAlias":"0x3","remoteAlias":"0x3"},"canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full index d41d006dd2..dff011caa1 100644 --- a/eclair-node/src/test/resources/api/findroute-full +++ b/eclair-node/src/test/resources/api/findroute-full @@ -1 +1 @@ -{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}]}]} \ No newline at end of file +{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}}]}]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/getinfo b/eclair-node/src/test/resources/api/getinfo index f0264a9768..c6a823114e 100644 --- a/eclair-node/src/test/resources/api/getinfo +++ b/eclair-node/src/test/resources/api/getinfo @@ -1 +1 @@ -{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["localhost:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"} \ No newline at end of file +{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["127.0.0.1:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/peers b/eclair-node/src/test/resources/api/peers index 3e12eddaa8..5fd9b2a1b8 100644 --- a/eclair-node/src/test/resources/api/peers +++ b/eclair-node/src/test/resources/api/peers @@ -1 +1 @@ -[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"localhost:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] \ No newline at end of file +[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"127.0.0.1:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired index d754eceb51..d1d4b57414 100644 --- a/eclair-node/src/test/resources/api/received-expired +++ b/eclair-node/src/test/resources/api/received-expired @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending index 9e43ab9b12..abc7fdb9f7 100644 --- a/eclair-node/src/test/resources/api/received-pending +++ b/eclair-node/src/test/resources/api/received-pending @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success index b54154b991..6fdc44e62d 100644 --- a/eclair-node/src/test/resources/api/received-success +++ b/eclair-node/src/test/resources/api/received-success @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/usablebalances b/eclair-node/src/test/resources/api/usablebalances index c1ef6b4920..2876388234 100644 --- a/eclair-node/src/test/resources/api/usablebalances +++ b/eclair-node/src/test/resources/api/usablebalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"unknown"},"localAlias":"0x1"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x2"},"localAlias":"0x3","remoteAlias":"0x4"},"canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index db3a12b910..dba2e0834a 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -24,8 +24,8 @@ import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe} import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, DataLossProtect} @@ -42,13 +42,15 @@ import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.relay.Relayer.UsableBalance +import fr.acinq.eclair.payment.relay.Relayer.ChannelBalance import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router -import fr.acinq.eclair.router.Router.PredefinedNodeRoute +import fr.acinq.eclair.router.Router.{ChannelRelayParams, PredefinedNodeRoute} import fr.acinq.eclair.wire.protocol._ +import org.json4s.{Formats, Serialization} import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import scodec.bits._ @@ -62,12 +64,12 @@ import scala.util.Try class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticMockito with Matchers { - implicit val formats = JsonSupport.formats - implicit val serialization = JsonSupport.serialization - implicit val routeTestTimeout = RouteTestTimeout(3 seconds) + implicit val formats: Formats = JsonSupport.formats + implicit val serialization: Serialization = JsonSupport.serialization + implicit val routeTestTimeout: RouteTestTimeout = RouteTestTimeout(3 seconds) - val aliceNodeId = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0") - val bobNodeId = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585") + val aliceNodeId: PublicKey = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0") + val bobNodeId: PublicKey = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585") object PluginApi extends RouteProvider { override def route(directives: EclairDirectives): Route = { @@ -167,7 +169,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ActorRef.noSender, nodeId = aliceNodeId, state = Peer.CONNECTED, - address = Some(NodeAddress.fromParts("localhost", 9731).get.socketAddress), + address = Some(NodeAddress.fromParts("localhost", 9731).get), channels = 1), PeerInfo( ActorRef.noSender, @@ -200,11 +202,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'usablebalances' asks relayer for current usable balances") { + test("'usablebalances' returns expected balance json only for enabled channels") { val eclair = mock[Eclair] eclair.usableBalances()(any[Timeout]) returns Future.successful(List( - UsableBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true), - UsableBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false) + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Unknown, Alias(1), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(2)), Alias(3), Some(Alias(4))), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) )) val mockService = mockApi(eclair) @@ -220,6 +222,26 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } + test("'channelbalances' returns expected balance json for all channels") { + val eclair = mock[Eclair] + eclair.channelBalances()(any[Timeout]) returns Future.successful(List( + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Temporary(RealShortChannelId(1)), Alias(2), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(3)), Alias(3), Some(Alias(3))), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) + )) + + val mockService = mockApi(eclair) + Post("/channelbalances") ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + Route.seal(mockService.channelBalances) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + eclair.channelBalances()(any[Timeout]).wasCalled(once) + matchTestJson("channelbalances", response) + } + } + test("'getinfo' response should include this node ID") { val eclair = mock[Eclair] val mockService = new MockService(eclair) @@ -353,18 +375,19 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), None, None, None)(any[Timeout]).wasCalled(once) } } test("'close' method should accept shortChannelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -379,19 +402,20 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Right(ShortChannelId(shortChannelIdSerialized)) :: Nil, None, None)(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } } test("'close' method should accept channelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -413,12 +437,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'close' method should accept channelIds and shortChannelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -433,22 +458,23 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Left(channelId) :: Right(ShortChannelId("42000x27x3")) :: Right(ShortChannelId("42000x561x1")) :: Nil, None, None)(any[Timeout]).wasCalled(once) + eclair.close(Left(channelId) :: Right(ShortChannelId.fromCoordinates("42000x27x3").success.value) :: Right(ShortChannelId.fromCoordinates("42000x561x1").success.value) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } } test("'close' accepts custom closing feerates 1") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2", "maxFeerateSatByte" -> "50").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2", "maxFeerateSatByte" -> "50").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -456,21 +482,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(12500 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 2") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -478,21 +505,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(5000 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 3") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -500,21 +528,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(5000 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 4") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "maxFeerateSatByte" -> "50").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "maxFeerateSatByte" -> "50").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -522,7 +551,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(12500 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } @@ -610,7 +639,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000"}""" - assert(response === expected) + assert(response == expected) } val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") @@ -624,7 +653,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" - assert(response === expected) + assert(response == expected) } val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) @@ -637,7 +666,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(response === expected) + assert(response == expected) } } @@ -803,7 +832,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 2") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = randomBytes32() eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) @@ -823,7 +852,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 3") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val expired = randomBytes32() eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) @@ -843,7 +872,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 4") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val received = randomBytes32() eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L))))) @@ -922,15 +951,15 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None) val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}""" val externalId = UUID.randomUUID().toString - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) val expectedRoute = PredefinedNodeRoute(Seq(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))) val jsonNodes = serialization.write(expectedRoute.nodes) val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) - Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> PaymentRequest.write(pr)).toEntity) ~> + Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> pr.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.sendToRoute) ~> @@ -938,23 +967,23 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == expected) - eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) } } test("'sendtoroute' method should accept a comma separated list of pubkeys") { val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None) val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}""" - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) val expectedRoute = PredefinedNodeRoute(Seq(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))) val csvNodes = "0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9, 0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3, 026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28" val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) // this test uses CSV encoded route - Post("/sendtoroute", FormData("nodeIds" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)).toEntity) ~> + Post("/sendtoroute", FormData("nodeIds" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> pr.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.sendToRoute) ~> @@ -962,18 +991,18 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == expected) - eclair.sendToRoute(1234 msat, None, None, None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(1234 msat, None, None, None, pr, expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) } } test("'findroute' method response should support nodeId, shortChannelId and full formats") { val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - val invoice = PaymentRequest.read(serializedInvoice) + val invoice = Invoice.fromString(serializedInvoice).get val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), - shortChannelId = ShortChannelId(BlockHeight(1), 2, 3), + shortChannelId = RealShortChannelId(BlockHeight(1), 2, 3), timestamp = 0 unixsec, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(0), @@ -982,10 +1011,12 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM feeProportionalMillionths = 1, htlcMaximumMsat = None ) + val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 4)) + val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 5)) - val mockHop1 = Router.ChannelHop(PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) - val mockHop2 = Router.ChannelHop(mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 4))) - val mockHop3 = Router.ChannelHop(mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 5))) + val mockHop1 = Router.ChannelHop(mockChannelUpdate1.shortChannelId, PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate1)) + val mockHop2 = Router.ChannelHop(mockChannelUpdate2.shortChannelId, mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate2)) + val mockHop3 = Router.ChannelHop(mockChannelUpdate3.shortChannelId, mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate3)) val mockHops = Seq(mockHop1, mockHop2, mockHop3) val eclair = mock[Eclair] @@ -1098,55 +1129,55 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(pf) === expectedSerializedPf) + assert(serialization.write(pf) == expectedSerializedPf) system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" - assert(serialization.write(ps) === expectedSerializedPs) + assert(serialization.write(ps) == expectedSerializedPs) system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784963659L)) val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(prel) === expectedSerializedPrel) + assert(serialization.write(prel) == expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentRelayed.Part(21 msat, ByteVector32.Zeroes)), Seq(PaymentRelayed.Part(8 msat, ByteVector32.Zeroes), PaymentRelayed.Part(10 msat, ByteVector32.One)), bobNodeId, 17 msat, TimestampMilli(1553784963659L)) val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"amount":21,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"}],"outgoing":[{"amount":8,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"},{"amount":10,"channelId":"0100000000000000000000000000000000000000000000000000000000000000"}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17,"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(ptrel) === expectedSerializedPtrel) + assert(serialization.write(ptrel) == expectedSerializedPtrel) system.eventStream.publish(ptrel) wsClient.expectMessage(expectedSerializedPtrel) val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L)))) val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}""" - assert(serialization.write(precv) === expectedSerializedPrecv) + assert(serialization.write(precv) == expectedSerializedPrecv) system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) val pset = PaymentSettlingOnChain(fixedUUID, 21 msat, ByteVector32.One, timestamp = TimestampMilli(1553785442676L)) val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T15:04:02.676Z","unix":1553785442}}""" - assert(serialization.write(pset) === expectedSerializedPset) + assert(serialization.write(pset) == expectedSerializedPset) system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) - val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isFunder = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) - val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isFunder":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" - assert(serialization.write(chcr) === expectedSerializedChcr) + val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isInitiator = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) + val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","commitTxFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" + assert(serialization.write(chcr) == expectedSerializedChcr) system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) val chsc = ChannelStateChanged(system.deadLetters, ByteVector32.One, system.deadLetters, bobNodeId, OFFLINE, NORMAL, null) val expectedSerializedChsc = """{"type":"channel-state-changed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","previousState":"OFFLINE","currentState":"NORMAL"}""" - assert(serialization.write(chsc) === expectedSerializedChsc) + assert(serialization.write(chsc) == expectedSerializedChsc) system.eventStream.publish(chsc) wsClient.expectMessage(expectedSerializedChsc) val chcl = ChannelClosed(system.deadLetters, ByteVector32.One, Closing.NextRemoteClose(null, null), null) val expectedSerializedChcl = """{"type":"channel-closed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","closingType":"NextRemoteClose"}""" - assert(serialization.write(chcl) === expectedSerializedChcl) + assert(serialization.write(chcl) == expectedSerializedChcl) system.eventStream.publish(chcl) wsClient.expectMessage(expectedSerializedChcl) @@ -1158,12 +1189,26 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM GenericTlv(UInt64(5), hex"1111") ))), Some(hex"2222")) val expectedSerializedMsgrcv = """{"type":"onion-message-received","pathId":"2222","encodedReplyPath":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358502eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619020303f91e620504cde242df38d04599d8b4d4c555149cc742a5f12de452cbdd400013126a26221759247584d704b382a5789f1d8c5a","replyPath":{"introductionNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","blindingKey":"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619","blindedNodes":[{"blindedPublicKey":"020303f91e620504cde242df38d04599d8b4d4c555149cc742a5f12de452cbdd40","encryptedPayload":"126a26221759247584d704b382a5789f1d8c5a"}]},"unknownTlvs":{"5":"1111"}}""" - assert(serialization.write(msgrcv) === expectedSerializedMsgrcv) + assert(serialization.write(msgrcv) == expectedSerializedMsgrcv) system.eventStream.publish(msgrcv) wsClient.expectMessage(expectedSerializedMsgrcv) } } + test("stop eclair") { + val eclair = mock[Eclair] + val mockService = new MockService(eclair) + eclair.stop() returns Future.successful(()) + + Post("/stop") ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + Route.seal(mockService.stop) ~> + check { + assert(handled) + assert(status == OK) + } + } + private def matchTestJson(apiName: String, response: String) = { val resource = getClass.getResourceAsStream(s"/api/$apiName") val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse { diff --git a/monitoring/grafana-dashboard/host-metrics.json b/monitoring/grafana-dashboard/host-metrics.json new file mode 100644 index 0000000000..d48379d44b --- /dev/null +++ b/monitoring/grafana-dashboard/host-metrics.json @@ -0,0 +1,993 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.0.1" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "panel", + "id": "piechart", + "name": "Pie chart", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 36, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_load_average" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 34, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "max(rate(host_load_average[1m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Load Average", + "type": "timeseries" + } + ], + "title": "Load Average", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 32, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [ { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "host_swap_usage" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 2 + }, + "id": 30, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_swap_usage[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Swap Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [ { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "host_memory_usage" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 2 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_memory_usage[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "percent" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{__name__=\"host_storage_mount_space_total_bytes\", component=\"host\", job=\"eclair\", mount=\"/\"}", + "{__name__=\"host_storage_mount_space_free_bytes\", component=\"host\", job=\"eclair\", mount=\"/\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 26, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "bottom", + "values": [ + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "host_storage_mount_space_total_bytes", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "host_storage_mount_space_free_bytes", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host Storage", + "type": "piechart" + } + ], + "title": "Memory and File System Usage", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 22, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_data_read_bytes" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_data_write_bytes" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 3 + }, + "id": 20, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_data_read_bytes_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_data_write_bytes_total[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host network data read/write bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_packets_read" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_packets_write" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 3 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_packets_read_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_packets_write_total[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host network packet read/write", + "type": "timeseries" + } + ], + "title": "Network Usage", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 16, + "panels": [], + "title": "CPU Usage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_cpu_system_usage" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_cpu_user_usage" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "max", + "min", + "firstNotNull", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_cpu_usage_sum{mode=\"system\"}[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_cpu_usage_sum{mode=\"user\"}[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Overall", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Eclair Host Metrics", + "uid": "FDdGZp37k", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index cf6eb02664..adb6006aea 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.1-SNAPSHOT pom @@ -67,14 +67,14 @@ UTF-8 11 11 - 2.13.6 + 2.13.8 2.13 - 2.6.15 - 10.2.4 - 1.7.2 - 0.19 - 24.0-android - 2.2.2 + 2.6.18 + 10.2.7 + 3.4.1 + 0.23 + 31.1-jre + 2.4.6 @@ -132,7 +132,7 @@ net.alchim31.maven scala-maven-plugin - 3.4.2 + 4.6.2 -feature @@ -228,14 +228,14 @@ org.scalatest scalatest-maven-plugin - 2.0.0 + 2.0.2 true I ${project.build.directory} - -Xmx1024m -Dfile.encoding=UTF-8 + -Xmx2048m -Dfile.encoding=UTF-8 @@ -274,7 +274,7 @@ org.scalatest scalatest_${scala.version.short} - 3.1.1 + 3.2.12 test