diff --git a/.codecov.yml b/.codecov.yml index bb800e2965..a33d03b813 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,4 +1,6 @@ coverage: + ci: + - pm47.semaphoreci.com status: project: off patch: off diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml new file mode 100644 index 0000000000..dd93f52060 --- /dev/null +++ b/.semaphore/semaphore.yml @@ -0,0 +1,29 @@ +version: v1.0 +name: Build & Test +agent: + machine: + type: e1-standard-4 + os_image: ubuntu1804 +execution_time_limit: + minutes: 15 + +blocks: + - name: Build & Test + task: + secrets: + # This needs to have the same name as the secret entry configured on semaphore dashboard. + - name: Codecov upload token + env_vars: + # Set maven to use a local directory. This is required for + # the cache util. It must be set in all blocks. + - name: MAVEN_OPTS + value: "-Dmaven.repo.local=.m2" + jobs: + - name: Build & Test + commands: + - sem-version java 11 + - checkout + - cache restore maven + - mvn scoverage:report + - bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN + - cache store maven .m2 diff --git a/.travis.yml b/.travis.yml index e037fd7592..7bd9c28c35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,9 @@ scala: env: - export LD_LIBRARY_PATH=/usr/local/lib before_install: - - wget http://apache.crihan.fr/dist/maven/maven-3/3.6.0/binaries/apache-maven-3.6.0-bin.zip - - unzip -qq apache-maven-3.6.0-bin.zip - - export M2_HOME=$PWD/apache-maven-3.6.0 + - wget https://apache.osuosl.org/maven/maven-3/3.6.2/binaries/apache-maven-3.6.2-bin.zip + - unzip -qq apache-maven-3.6.2-bin.zip + - export M2_HOME=$PWD/apache-maven-3.6.2 - export PATH=$M2_HOME/bin:$PATH script: - mvn scoverage:report @@ -22,6 +22,4 @@ jdk: - openjdk11 notifications: email: - - ops@acinq.fr -after_success: - - bash <(curl -s https://codecov.io/bash) + - ops@acinq.fr \ No newline at end of file diff --git a/BUILD.md b/BUILD.md index 0c050418eb..456e5a9af3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,39 +1,55 @@ # Building Eclair ## Requirements -- [OpenJDK 11](https://jdk.java.net/11/). + +- [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). - [Maven](https://maven.apache.org/download.cgi) 3.6.0 or newer - [Docker](https://www.docker.com/) 18.03 or newer (optional) if you want to run all tests -:warning: You can also use [Oracle JDK 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) to build and run eclair, but we recommend you use Open JDK11. - ## Build -To build the project, simply run: + +To build the project and run the tests, simply run: + ```shell -$ mvn install +mvn install ``` -#### Other build options +### Other build options To skip all tests, run: + ```shell -$ mvn install -DskipTests +mvn install -DskipTests ``` -To only build the `eclair-node` module + +To only build the `eclair-node` module, run: + +```shell +mvn install -pl eclair-node -am -DskipTests +``` + +To run the tests, run: + +```shell +mvn test +``` + +To run tests for a specific class, run: + ```shell -$ mvn install -pl eclair-node -am -DskipTests +mvn test -Dsuites=* ``` -# Building the API documentation +## Build the API documentation -## Slate +### Slate The API doc is generated via slate and hosted on github pages. To make a change and update the doc follow the steps: -1. git checkout slate-doc -2. Install your local dependencies for slate, more info [here](https://github.com/lord/slate#getting-started-with-slate) -3. Edit `source/index.html.md` and save your changes. -4. Commit all the changes to git, before deploying the repo should be clean. -5. Push your commit to remote. -6. Run `./deploy.sh` -7. Wait a few minutes and the doc should be updated at https://acinq.github.io/eclair \ No newline at end of file +1. `git checkout slate-doc` +2. Install your local dependencies for slate, more info [here](https://github.com/lord/slate#getting-started-with-slate) +3. Edit `source/index.html.md` and save your changes. +4. Commit all the changes to git, before deploying the repo should be clean. +5. Push your commit to remote. +6. Run `./deploy.sh` +7. Wait a few minutes and the doc should be updated at [https://acinq.github.io/eclair](https://acinq.github.io/eclair) diff --git a/Dockerfile b/Dockerfile index 475a939822..7d33eb265e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,13 @@ -FROM openjdk:8u171-jdk-alpine as BUILD +FROM adoptopenjdk/openjdk11:jdk-11.0.3_7-alpine 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 apk add --no-cache curl tar bash -ARG MAVEN_VERSION=3.6.0 +ARG MAVEN_VERSION=3.6.2 ARG USER_HOME_DIR="/root" -ARG SHA=6a1b346af36a1f1a491c1c1a141667c5de69b42e6611d3687df26868bc0f4637 +ARG SHA=3fbc92d1961482d6fbd57fbf3dd6d27a4de70778528ee3fb44aa7d27eb32dfdc ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries RUN mkdir -p /usr/share/maven /usr/share/maven/ref \ @@ -42,7 +41,7 @@ RUN mvn package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.comm # 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:8u181-jre-slim +FROM openjdk:11.0.4-jre-slim WORKDIR /app # install jq for eclair-cli diff --git a/README.md b/README.md index cf308835b6..c2c6c7739b 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,20 @@ **Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON API is also available. This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning) and [lnd](https://github.com/LightningNetwork/lnd). - - --- - - :construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change! - - :rotating_light: If you run Eclair on mainnet (which is the default setting): - - Keep in mind that it is beta-quality software and **don't put too much money** in it - - Eclair's JSON API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) - + +--- + +:construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change! + +:rotating_light: If you run Eclair on mainnet (which is the default setting): + +* Keep in mind that it is beta-quality software and **don't put too much money** in it +* Eclair's JSON API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API) + --- ## Lightning Network Specification Compliance + Please see the latest [release note](https://github.com/ACINQ/eclair/releases) for detailed information on BOLT compliance. ## Overview @@ -32,18 +34,26 @@ Eclair offers a feature rich HTTP API that enables application developers to eas For more information please visit the [API documentation website](https://acinq.github.io/eclair). +## Documentation + +Please visit our [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. + +You will find detailed guides and frequently asked questions there. + ## Installation ### Configuring Bitcoin Core :warning: Eclair requires Bitcoin Core 0.17.1 or higher. If you are upgrading an existing wallet, you need to create a new address and send all your funds to that address. -Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. +Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet. You can configure your Bitcoin Node to use either `p2sh-segwit` addresses or `bech32` addresses, Eclair is compatible with both modes. Run bitcoind with the following minimal `bitcoin.conf`: -``` + +```conf server=1 rpcuser=foo rpcpassword=bar @@ -55,17 +65,22 @@ zmqpubrawtx=tcp://127.0.0.1:29000 ### Installing Eclair Eclair is developed in [Scala](https://www.scala-lang.org/), a powerful functional language that runs on the JVM, and is packaged as a JAR (Java Archive) file. We provide 2 different packages, which internally use the same core libraries: + * eclair-node, which is a headless application that you can run on servers and desktops, and control from the command line * eclair-node-gui, which also includes a JavaFX GUI -To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). Eclair will also run on Oracle JDK 1.8, Oracle JDK 11, and other versions of OpenJDK but we don't recommend using them. +To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot). Other runtimes also work but we don't recommend using them. Then download our latest [release](https://github.com/ACINQ/eclair/releases) and depending on whether or not you want a GUI run the following command: + * with GUI: + ```shell java -jar eclair-node-gui--.jar ``` + * without GUI: + ```shell java -jar eclair-node--.jar ``` @@ -78,7 +93,7 @@ Eclair reads its configuration file, and write its logs, to `~/.eclair` by defau To change your node's configuration, create a file named `eclair.conf` in `~/.eclair`. Here's an example configuration file: -``` +```conf eclair.node-alias=eclair eclair.node-color=49daaa ``` @@ -111,10 +126,11 @@ Some advanced parameters can be changed with java environment variables. Most us name | description | default value ----------------------|--------------------------------------------|-------------- eclair.datadir | Path to the data directory | ~/.eclair -eclair.headless | Run eclair without a GUI | +eclair.headless | Run eclair without a GUI | eclair.printToConsole | Log to stdout (in addition to eclair.log) | For example, to specify a different data directory you would run the following command: + ```shell java -Declair.datadir=/tmp/node1 -jar eclair-node-gui--.jar ``` @@ -130,41 +146,44 @@ java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gu #### Backup The files that you need to backup are located in your data directory. You must backup: -- your seed (`seed.dat`) -- your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) + +* your seed (`seed.dat`) +* your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) Your seed never changes once it has been created, but your channels will change whenever you receive or send payments. Eclair will -create and maintain a snapshot of its database, named `eclair.sqlite.bak`, in your data directory, and update it when needed. This file is +create and maintain a snapshot of its database, named `eclair.sqlite.bak`, in your data directory, and update it when needed. This file is always consistent and safe to use even when Eclair is running, and this is what you should backup regularly. For example you could configure a `cron` task for your backup job. Or you could configure an optional notification script to be called by eclair once a new database snapshot has been created, using the following option: -``` + +```conf eclair.backup-notify-script = "/absolute/path/to/script.sh" ``` + Make sure that your script is executable and uses an absolute path name for `eclair.sqlite.bak`. -Note that depending on your filesystem, in your backup process we recommend first moving `eclair.sqlite.bak` to some temporary file +Note that depending on your filesystem, in your backup process we recommend first moving `eclair.sqlite.bak` to some temporary file 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. You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`. -``` +```shell docker run -ti --rm -e "JAVA_OPTS=-Xmx512m -Declair.api.binding-ip=0.0.0.0 -Declair.node-alias=node-pm -Declair.printToConsole" acinq/eclair ``` If you want to persist the data directory, you can make the volume to your host with the `-v` argument, as the following example: -``` +```shell 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: -``` + +```shell docker exec eclair-cli -p foobar getinfo ``` @@ -175,6 +194,7 @@ For advanced usage, Eclair supports plugins written in Scala, Java, or any JVM-c A valid plugin is a jar that contains an implementation of the [Plugin](eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala) interface. Here is how to run Eclair with plugins: + ```shell java -jar eclair-node--.jar <...> ``` @@ -184,15 +204,15 @@ java -jar eclair-node--.jar <... 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 eclair.chain=testnet 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, +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: -``` +```conf server=1 txindex=1 [main] @@ -208,6 +228,7 @@ zmqpubrawtx=tcp://127.0.0.1:29001 ``` ## Resources -- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja -- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell -- [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to + +* [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja +* [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell +* [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 08984c7ab4..be2102d71c 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -27,11 +27,12 @@ where OPTIONS can be: -s Some commands can print a trimmed JSON and COMMAND is one of: - getinfo, connect, open, close, forceclose, updaterelayfee, + getinfo, connect, disconnect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates findroute, findroutetonode, parseinvoice, payinvoice, sendtonode, - getsentinfo, createinvoice, getinvoice, listinvoices, - listpendinginvoices, getreceivedinfo, audit, networkfees, channelstats + sendtoroute, getsentinfo, createinvoice, getinvoice, listinvoices, + listpendinginvoices, getreceivedinfo, audit, networkfees, + channelstats, usablebalances Examples -------- @@ -88,7 +89,7 @@ jq_filter='if type=="object" and .error != null then .error else .'; # apply special jq filter if we are in "short" ouput mode -- only for specific commands such as 'channels' if [ "$short" = true ]; then - jq_channel_filter="{ nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocalMsat / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }"; + jq_channel_filter="{ nodeId, shortChannelId: .data.shortChannelId, channelId, state, balanceSat: (try (.data.commitments.localCommit.spec.toLocal / 1000 | floor) catch null), capacitySat: .data.commitments.commitInput.amountSatoshis, channelPoint: .data.commitments.commitInput.outPoint }"; case $api_endpoint in "channels") jq_filter="$jq_filter | map( $jq_channel_filter )" ;; "channel") jq_filter="$jq_filter | $jq_channel_filter" ;; diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 0bb3ae62a9..4a25e8768d 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.18.1/bitcoin-0.18.1-x86_64-linux-gnu.tar.gz - 724043999e2b5ed0c088e8db34f15d43 - 546ee35d4089c7ccc040a01cdff3362599b8bc53 + d3159a28702ca0cba2e0459e83219dfb + 969020835c1f0c759032def0d7b99669db06d8f7 @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.18.1/bitcoin-0.18.1-osx64.tar.gz - b5a792c6142995faa42b768273a493bd - 8bd51c7024d71de07df381055993e9f472013db8 + 0334b1024f28e83341c89df14e622bb6 + 80354b40b409f342f5d35acd6b2c0e71f689285b @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip - b0e824e9dd02580b5b01f073f3c89858 - 4e17bad7d08c465b444143a93cd6eb1c95076e3f + https://bitcoin.org/bin/bitcoin-core-0.18.1/bitcoin-0.18.1-win64.zip + 637776ca50b4354ca2f523bdee576bdb + 44771cc2161853b5230a7a159278dc8f27e7d4c2 @@ -152,7 +152,7 @@ io.netty netty-all - 4.1.32.Final + 4.1.42.Final @@ -210,6 +210,7 @@ guava ${guava.version} + com.softwaremill.quicklens diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index e1c019a49b..2e3c1746f2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -32,8 +32,8 @@ eclair { node-alias = "eclair" node-color = "49daaa" - global-features = "" - local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + global-features = "0200" // variable_length_onion + local-features = "088a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -41,6 +41,7 @@ eclair { # local-features = "" # } ] + sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers channel-flags = 0 // do not announce channels dust-limit-satoshis = 546 @@ -86,6 +87,7 @@ eclair { // maximum local vs remote feerate mismatch; 1.0 means 100% // actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch max-feerate-mismatch = 1.56 // will allow remote fee rates up to 8x bigger or smaller than our local fee rate + 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. @@ -112,8 +114,16 @@ eclair { randomize-route-selection = true // when computing a route for a payment we randomize the final selection 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 + network-stats-interval = 6 hours // frequency at which we refresh global network statistics (expensive operation) init-timeout = 5 minutes + 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 = 2500 // 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* + } + // the values below will be used to perform route searching path-finding { max-route-length = 6 // max route length for the 'first pass', if none is found then a second pass is made with no limit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/CheckElectrumSetup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/CheckElectrumSetup.scala index b3156f612f..16d9d9937f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/CheckElectrumSetup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/CheckElectrumSetup.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair import java.io.File import java.net.InetSocketAddress +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, PoisonPill, Props, ReceiveTimeout, SupervisorStrategy} import com.typesafe.config.{Config, ConfigFactory} @@ -27,7 +28,7 @@ import fr.acinq.eclair.NodeParams.ELECTRUM import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{SSL, computeScriptHash} import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumClientPool} -import fr.acinq.eclair.blockchain.fee.FeeEstimator +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKB, FeeratesPerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db.Databases @@ -58,12 +59,31 @@ class CheckElectrumSetup(datadir: File, case Some(d) => d case None => Databases.sqliteJDBC(new File(datadir, chain)) } + /** + * This counter holds the current blockchain height. + * It is mainly used to calculate htlc expiries. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val blockCount = new AtomicLong(0) + + /** + * This holds the current feerates, in satoshi-per-kilobytes. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKB = new AtomicReference[FeeratesPerKB](null) + + /** + * This holds the current feerates, in satoshi-per-kw. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKw = new AtomicReference[FeeratesPerKw](null) + val feeEstimator = new FeeEstimator { - override def getFeeratePerKb(target: Int): Long = Globals.feeratesPerKB.get().feePerBlock(target) - override def getFeeratePerKw(target: Int): Long = Globals.feeratesPerKw.get().feePerBlock(target) + override def getFeeratePerKb(target: Int): Long = feeratesPerKB.get().feePerBlock(target) + override def getFeeratePerKw(target: Int): Long = feeratesPerKw.get().feePerBlock(target) } - val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, feeEstimator) + val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, blockCount, feeEstimator) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") @@ -103,7 +123,7 @@ class CheckElectrumSetup(datadir: File, val stream = classOf[Setup].getResourceAsStream(addressesFile) ElectrumClientPool.readServerAddresses(stream, sslEnabled) } - val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(addresses)), "electrum-client", SupervisorStrategy.Resume)) + val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(blockCount, addresses)), "electrum-client", SupervisorStrategy.Resume)) electrumClient case _ => ??? } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/CltvExpiry.scala b/eclair-core/src/main/scala/fr/acinq/eclair/CltvExpiry.scala new file mode 100644 index 0000000000..d3249fd461 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/CltvExpiry.scala @@ -0,0 +1,61 @@ +/* + * 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 + +/** + * Created by t-bast on 21/08/2019. + */ + +/** + * Bitcoin scripts (in particular HTLCs) need an absolute block expiry (greater than the current block count) to work + * with OP_CLTV. + * + * @param underlying the absolute cltv expiry value (current block count + some delta). + */ +case class CltvExpiry(private val underlying: Long) extends Ordered[CltvExpiry] { + // @formatter:off + def +(d: CltvExpiryDelta): CltvExpiry = CltvExpiry(underlying + d.toInt) + def -(d: CltvExpiryDelta): CltvExpiry = CltvExpiry(underlying - d.toInt) + def -(other: CltvExpiry): CltvExpiryDelta = CltvExpiryDelta((underlying - other.underlying).toInt) + override def compare(other: CltvExpiry): Int = underlying.compareTo(other.underlying) + def toLong: Long = underlying + // @formatter:on +} + +/** + * Channels advertise a cltv expiry delta that should be used when routing through them. + * This value needs to be converted to a [[fr.acinq.eclair.CltvExpiry]] to be used in OP_CLTV. + * + * CltvExpiryDelta can also be used when working with OP_CSV which is by design a delta. + * + * @param underlying the cltv expiry delta value. + */ +case class CltvExpiryDelta(private val underlying: Int) extends Ordered[CltvExpiryDelta] { + + /** + * Adds the current block height to the given delta to obtain an absolute expiry. + */ + def toCltvExpiry(blockHeight: Long) = CltvExpiry(blockHeight + underlying) + + // @formatter:off + def +(other: Int): CltvExpiryDelta = CltvExpiryDelta(underlying + other) + def +(other: CltvExpiryDelta): CltvExpiryDelta = CltvExpiryDelta(underlying + other.underlying) + def compare(other: CltvExpiryDelta): Int = underlying.compareTo(other.underlying) + def toInt: Int = underlying + // @formatter:on + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala index e4453ab8aa..c12bd3ba8d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/CoinUtils.scala @@ -18,15 +18,15 @@ package fr.acinq.eclair import java.text.{DecimalFormat, NumberFormat} -import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi} import grizzled.slf4j.Logging import scala.util.{Failure, Success, Try} /** - * Internal UI utility class, useful for lossless conversion between BtcAmount. - * The issue being that Satoshi contains a Long amount and it can not be converted to MilliSatoshi without losing the decimal part. - */ + * Internal UI utility class, useful for lossless conversion between BtcAmount. + * The issue being that Satoshi contains a Long amount and it can not be converted to MilliSatoshi without losing the decimal part. + */ private sealed trait BtcAmountGUILossless { def amount_msat: Long def unit: CoinUnit @@ -126,15 +126,15 @@ object CoinUtils extends Logging { } /** - * Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if - * it has too many decimals because MilliSatoshi only accepts Long amount. - * - * @param amount numeric String, can be decimal. - * @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC. - * @return amount as a MilliSatoshi object. - * @throws NumberFormatException if the amount parameter is not numeric. - * @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC. - */ + * Converts a string amount denominated in a bitcoin unit to a Millisatoshi amount. The amount might be truncated if + * it has too many decimals because MilliSatoshi only accepts Long amount. + * + * @param amount numeric String, can be decimal. + * @param unit bitcoin unit, can be milliSatoshi, Satoshi, Bits, milliBTC, BTC. + * @return amount as a MilliSatoshi object. + * @throws NumberFormatException if the amount parameter is not numeric. + * @throws IllegalArgumentException if the unit is not equals to milliSatoshi, Satoshi or milliBTC. + */ @throws(classOf[NumberFormatException]) @throws(classOf[IllegalArgumentException]) def convertStringAmountToMsat(amount: String, unit: String): MilliSatoshi = { @@ -154,13 +154,11 @@ object CoinUtils extends Logging { } def convertStringAmountToSat(amount: String, unit: String): Satoshi = - fr.acinq.bitcoin.millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit)) + CoinUtils.convertStringAmountToMsat(amount, unit).truncateToSatoshi /** - * Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported. - * @param unit - * @return - */ + * Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported. + */ def getUnitFromString(unit: String): CoinUnit = unit.toLowerCase() match { case u if u == MSatUnit.code || u == MSatUnit.label.toLowerCase() => MSatUnit case u if u == SatUnit.code || u == SatUnit.label.toLowerCase() => SatUnit @@ -171,73 +169,81 @@ object CoinUtils extends Logging { } /** - * Converts BtcAmount to a GUI Unit (wrapper containing amount as a millisatoshi long) - * - * @param amount BtcAmount - * @param unit unit to convert to - * @return a GUICoinAmount - */ + * Converts BtcAmount to a GUI Unit (wrapper containing amount as a millisatoshi long) + * + * @param amount BtcAmount + * @param unit unit to convert to + * @return a GUICoinAmount + */ private def convertAmountToGUIUnit(amount: BtcAmount, unit: CoinUnit): BtcAmountGUILossless = (amount, unit) match { // amount is msat, so no conversion required - case (a: MilliSatoshi, MSatUnit) => GUIMSat(a.amount * MSatUnit.factorToMsat) - case (a: MilliSatoshi, SatUnit) => GUISat(a.amount * MSatUnit.factorToMsat) - case (a: MilliSatoshi, BitUnit) => GUIBits(a.amount * MSatUnit.factorToMsat) - case (a: MilliSatoshi, MBtcUnit) => GUIMBtc(a.amount * MSatUnit.factorToMsat) - case (a: MilliSatoshi, BtcUnit) => GUIBtc(a.amount * MSatUnit.factorToMsat) + case (a: MilliSatoshi, MSatUnit) => GUIMSat(a.toLong * MSatUnit.factorToMsat) + case (a: MilliSatoshi, SatUnit) => GUISat(a.toLong * MSatUnit.factorToMsat) + case (a: MilliSatoshi, BitUnit) => GUIBits(a.toLong * MSatUnit.factorToMsat) + case (a: MilliSatoshi, MBtcUnit) => GUIMBtc(a.toLong * MSatUnit.factorToMsat) + case (a: MilliSatoshi, BtcUnit) => GUIBtc(a.toLong * MSatUnit.factorToMsat) // amount is satoshi, convert sat -> msat - case (a: Satoshi, MSatUnit) => GUIMSat(a.amount * SatUnit.factorToMsat) - case (a: Satoshi, SatUnit) => GUISat(a.amount * SatUnit.factorToMsat) - case (a: Satoshi, BitUnit) => GUIBits(a.amount * SatUnit.factorToMsat) - case (a: Satoshi, MBtcUnit) => GUIMBtc(a.amount * SatUnit.factorToMsat) - case (a: Satoshi, BtcUnit) => GUIBtc(a.amount * SatUnit.factorToMsat) + case (a: Satoshi, MSatUnit) => GUIMSat(a.toLong * SatUnit.factorToMsat) + case (a: Satoshi, SatUnit) => GUISat(a.toLong * SatUnit.factorToMsat) + case (a: Satoshi, BitUnit) => GUIBits(a.toLong * SatUnit.factorToMsat) + case (a: Satoshi, MBtcUnit) => GUIMBtc(a.toLong * SatUnit.factorToMsat) + case (a: Satoshi, BtcUnit) => GUIBtc(a.toLong * SatUnit.factorToMsat) // amount is mbtc - case (a: MilliBtc, MSatUnit) => GUIMSat((a.amount * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, SatUnit) => GUISat((a.amount * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, BitUnit) => GUIBits((a.amount * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, MBtcUnit) => GUIMBtc((a.amount * MBtcUnit.factorToMsat).toLong) - case (a: MilliBtc, BtcUnit) => GUIBtc((a.amount * MBtcUnit.factorToMsat).toLong) + case (a: MilliBtc, MSatUnit) => GUIMSat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) + case (a: MilliBtc, SatUnit) => GUISat((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) + case (a: MilliBtc, BitUnit) => GUIBits((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) + case (a: MilliBtc, MBtcUnit) => GUIMBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) + case (a: MilliBtc, BtcUnit) => GUIBtc((a.toBigDecimal * MBtcUnit.factorToMsat).toLong) // amount is mbtc - case (a: Btc, MSatUnit) => GUIMSat((a.amount * BtcUnit.factorToMsat).toLong) - case (a: Btc, SatUnit) => GUISat((a.amount * BtcUnit.factorToMsat).toLong) - case (a: Btc, BitUnit) => GUIBits((a.amount * BtcUnit.factorToMsat).toLong) - case (a: Btc, MBtcUnit) => GUIMBtc((a.amount * BtcUnit.factorToMsat).toLong) - case (a: Btc, BtcUnit) => GUIBtc((a.amount * BtcUnit.factorToMsat).toLong) + case (a: Btc, MSatUnit) => GUIMSat((a.toBigDecimal * BtcUnit.factorToMsat).toLong) + case (a: Btc, SatUnit) => GUISat((a.toBigDecimal * BtcUnit.factorToMsat).toLong) + case (a: Btc, BitUnit) => GUIBits((a.toBigDecimal * BtcUnit.factorToMsat).toLong) + case (a: Btc, MBtcUnit) => GUIMBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong) + case (a: Btc, BtcUnit) => GUIBtc((a.toBigDecimal * BtcUnit.factorToMsat).toLong) - case (a, _) => + case (_, _) => throw new IllegalArgumentException(s"unhandled conversion from $amount to $unit") } /** - * Converts the amount to the user preferred unit and returns a localized formatted String. - * This method is useful for read only displays. - * - * @param amount BtcAmount - * @param withUnit if true, append the user unit shortLabel (mBTC, BTC, mSat...) - * @return formatted amount - */ + * Converts the amount to the user preferred unit and returns a localized formatted String. + * This method is useful for read only displays. + * + * @param amount BtcAmount + * @param withUnit if true, append the user unit shortLabel (mBTC, BTC, mSat...) + * @return formatted amount + */ def formatAmountInUnit(amount: BtcAmount, unit: CoinUnit, withUnit: Boolean = false): String = { val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit)) if (withUnit) s"$formatted ${unit.shortLabel}" else formatted } + def formatAmountInUnit(amount: MilliSatoshi, unit: CoinUnit, withUnit: Boolean): String = { + val formatted = COIN_FORMAT.format(rawAmountInUnit(amount, unit)) + if (withUnit) s"$formatted ${unit.shortLabel}" else formatted + } + /** - * Converts the amount to the user preferred unit and returns the BigDecimal value. - * This method is useful to feed numeric text input without formatting. - * - * Returns -1 if the given amount can not be converted. - * - * @param amount BtcAmount - * @return BigDecimal value of the BtcAmount - */ + * Converts the amount to the user preferred unit and returns the BigDecimal value. + * This method is useful to feed numeric text input without formatting. + * + * Returns -1 if the given amount can not be converted. + * + * @param amount BtcAmount + * @return BigDecimal value of the BtcAmount + */ def rawAmountInUnit(amount: BtcAmount, unit: CoinUnit): BigDecimal = Try(convertAmountToGUIUnit(amount, unit) match { case a: BtcAmountGUILossless => BigDecimal(a.amount_msat) / a.unit.factorToMsat case a => throw new IllegalArgumentException(s"unhandled unit $a") }) match { case Success(b) => b - case Failure(t) => logger.error("can not convert amount to user unit", t) + case Failure(t) => + logger.error("can not convert amount to user unit", t) -1 } + + def rawAmountInUnit(msat: MilliSatoshi, unit: CoinUnit): BigDecimal = BigDecimal(msat.toLong) / unit.factorToMsat } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala index b6f9c50da7..9a3b7ef8e3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala @@ -39,7 +39,7 @@ object DBCompatChecker extends Logging { * @param nodeParams */ def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = - Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes(), nodeParams.db.network.listChannelUpdates()) match { + Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes()) match { 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 36ce47b515..c235c9fbec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -22,21 +22,22 @@ import akka.actor.ActorRef import akka.pattern._ import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.TimestampQueryFilters._ import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer} -import fr.acinq.eclair.payment.PaymentLifecycle._ +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest +import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import scodec.bits.ByteVector -import scala.concurrent.Future import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) @@ -45,28 +46,31 @@ case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], case class TimestampQueryFilters(from: Long, to: Long) object TimestampQueryFilters { + /** We use this in the context of timestamp filtering, when we don't need an upper bound. */ + val MaxEpochMilliseconds = Duration.fromNanos(Long.MaxValue).toMillis + def getDefaultTimestampFilters(from_opt: Option[Long], to_opt: Option[Long]) = { - val from = from_opt.getOrElse(0L) - val to = to_opt.getOrElse(MaxEpochSeconds) + // NB: we expect callers to use seconds, but internally we use milli-seconds everywhere. + val from = from_opt.getOrElse(0L).seconds.toMillis + val to = to_opt.map(_.seconds.toMillis).getOrElse(MaxEpochMilliseconds) TimestampQueryFilters(from, to) } } - trait Eclair { def connect(target: Either[NodeURI, PublicKey])(implicit timeout: Timeout): Future[String] def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat_opt: Option[Long], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] + def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[String] def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String] - def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] + def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] @@ -74,17 +78,17 @@ trait Eclair { def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] - def receive(description: String, amountMsat_opt: Option[Long], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] + def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] - def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Long] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] + def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID] def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] - def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] - def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] + def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] @@ -111,7 +115,10 @@ trait Eclair { class EclairImpl(appKit: Kit) extends Eclair { - implicit val ec = appKit.system.dispatcher + implicit val ec: ExecutionContext = appKit.system.dispatcher + + // We constrain external identifiers. This allows uuid, long and pubkey to be used. + private val externalIdMaxLength = 66 override def connect(target: Either[NodeURI, PublicKey])(implicit timeout: Timeout): Future[String] = target match { case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[String] @@ -122,13 +129,13 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat_opt: Option[Long], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeerateSatByte_opt: Option[Long], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds)) (appKit.switchboard ? Peer.OpenChannel( remoteNodeId = nodeId, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = pushMsat_opt.map(MilliSatoshi).getOrElse(MilliSatoshi(0)), + fundingSatoshis = fundingAmount, + pushMsat = pushAmount_opt.getOrElse(0 msat), fundingTxFeeratePerKw_opt = fundingFeerateSatByte_opt.map(feerateByte2Kw), channelFlags = flags_opt.map(_.toByte), timeout_opt = Some(openTimeout))).mapTo[String] @@ -142,7 +149,7 @@ class EclairImpl(appKit: Kit) extends Eclair { sendToChannel(channelIdentifier, CMD_FORCECLOSE).mapTo[String] } - override def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = { + override def updateRelayFee(channelIdentifier: Either[ByteVector32, ShortChannelId], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = { sendToChannel(channelIdentifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String] } @@ -177,39 +184,51 @@ class EclairImpl(appKit: Kit) extends Eclair { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } - override def receive(description: String, amountMsat_opt: Option[Long], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { + override def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { 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(description = description, amountMsat_opt = amountMsat_opt.map(MilliSatoshi), expirySeconds_opt = expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] + (appKit.paymentHandler ? ReceivePayment(description = description, amount_opt = amount_opt, expirySeconds_opt = expire_opt, fallbackAddress = fallbackAddress_opt, paymentPreimage = paymentPreimage_opt)).mapTo[PaymentRequest] } - override def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = { - (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = { + (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse] } - override def sendToRoute(route: Seq[PublicKey], amountMsat: Long, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] = { - (appKit.paymentInitiator ? SendPaymentToRoute(amountMsat, paymentHash, route, finalCltvExpiry)).mapTo[UUID] + override def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = { + externalId_opt match { + case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters")) + case _ => (appKit.paymentInitiator ? SendPaymentRequest(amount, paymentHash, route.last, 1, finalCltvExpiryDelta, None, externalId_opt, route)).mapTo[UUID] + } } - override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThresholdSat_opt: Option[Long], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = { + override def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) - val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf) val routeParams = defaultRouteParams.copy( maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct), - maxFeeBaseMsat = feeThresholdSat_opt.map(_ * 1000).getOrElse(defaultRouteParams.maxFeeBaseMsat) + maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase) ) - val sendPayment = minFinalCltvExpiry_opt match { - case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams)) - case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams)) + externalId_opt match { + case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters")) + case _ => invoice_opt match { + case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired")) + case Some(invoice) => + val sendPayment = invoice.minFinalCltvExpiryDelta match { + case Some(minFinalCltvExpiryDelta) => SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, minFinalCltvExpiryDelta, invoice_opt, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)) + case None => SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, paymentRequest = invoice_opt, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)) + } + (appKit.paymentInitiator ? sendPayment).mapTo[UUID] + case None => + val sendPayment = SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams)) + (appKit.paymentInitiator ? sendPayment).mapTo[UUID] + } } - (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future { id match { case Left(uuid) => appKit.nodeParams.db.payments.getOutgoingPayment(uuid).toSeq - case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoingPayments(paymentHash) + case Right(paymentHash) => appKit.nodeParams.db.payments.listOutgoingPayments(paymentHash) } } @@ -238,26 +257,24 @@ class EclairImpl(appKit: Kit) extends Eclair { override def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { val filter = getDefaultTimestampFilters(from_opt, to_opt) - appKit.nodeParams.db.payments.listPaymentRequests(filter.from, filter.to) + appKit.nodeParams.db.payments.listIncomingPayments(filter.from, filter.to).map(_.paymentRequest) } override def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { val filter = getDefaultTimestampFilters(from_opt, to_opt) - appKit.nodeParams.db.payments.listPendingPaymentRequests(filter.from, filter.to) + appKit.nodeParams.db.payments.listPendingIncomingPayments(filter.from, filter.to).map(_.paymentRequest) } override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.getPaymentRequest(paymentHash) + appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.paymentRequest) } /** - * Sends a request to a channel and expects a response - * - * @param channelIdentifier either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) - * @param request - * @return - */ + * Sends a request to a channel and expects a response + * + * @param channelIdentifier either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded) + */ def sendToChannel(channelIdentifier: Either[ByteVector32, ShortChannelId], request: Any)(implicit timeout: Timeout): Future[Any] = channelIdentifier match { case Left(channelId) => appKit.register ? Forward(channelId, request) case Right(shortChannelId) => appKit.register ? ForwardShortId(shortChannelId, request) @@ -267,7 +284,7 @@ class EclairImpl(appKit: Kit) extends Eclair { GetInfoResponse(nodeId = appKit.nodeParams.nodeId, alias = appKit.nodeParams.alias, chainHash = appKit.nodeParams.chainHash, - blockHeight = Globals.blockCount.intValue(), + blockHeight = appKit.nodeParams.currentBlockHeight.toInt, publicAddresses = appKit.nodeParams.publicAddresses) ) 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 ad93d5eb46..12e3aa3f19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -16,10 +16,7 @@ package fr.acinq.eclair - -import java.util.BitSet - -import scodec.bits.ByteVector +import scodec.bits.{BitVector, ByteVector} /** * Created by PM on 13/02/2017. @@ -38,21 +35,32 @@ object Features { val VARIABLE_LENGTH_ONION_MANDATORY = 8 val VARIABLE_LENGTH_ONION_OPTIONAL = 9 - val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 14 - val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 15 + val CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY = 10 + val CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL = 11 + + // Note that BitVector indexes from left to right whereas the specification indexes from right to left. + // This is why we have to reverse the bits to check if a feature is set. - def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) + def hasFeature(features: BitVector, bit: Int): Boolean = if (features.sizeLessThanOrEqual(bit)) false else features.reverse.get(bit) - def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) + def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(features.bits, bit) + + /** + * We currently don't distinguish mandatory and optional. Interpreting VARIABLE_LENGTH_ONION_MANDATORY strictly would + * be very restrictive and probably fork us out of the network. + * We may implement this distinction later, but for now both flags are interpreted as an optional support. + */ + def hasVariableLengthOnion(features: ByteVector): Boolean = hasFeature(features, VARIABLE_LENGTH_ONION_MANDATORY) || hasFeature(features, VARIABLE_LENGTH_ONION_OPTIONAL) /** * Check that the features that we understand are correctly specified, and that there are no mandatory features that - * we don't understand (even bits) + * we don't understand (even bits). */ - def areSupported(bitset: BitSet): Boolean = { - val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY) - for (i <- 0 until bitset.length() by 2) { - if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false + def areSupported(features: BitVector): Boolean = { + val supportedMandatoryFeatures = Set[Long](OPTION_DATA_LOSS_PROTECT_MANDATORY, VARIABLE_LENGTH_ONION_MANDATORY) + val reversed = features.reverse + for (i <- 0L until reversed.length by 2) { + if (reversed.get(i) && !supportedMandatoryFeatures.contains(i)) return false } true @@ -62,5 +70,6 @@ object Features { * A feature set is supported if all even bits are supported. * We just ignore unknown odd bits. */ - def areSupported(features: ByteVector): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray)) + def areSupported(features: ByteVector): Boolean = areSupported(features.bits) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala deleted file mode 100644 index 5cc3630947..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Globals.scala +++ /dev/null @@ -1,48 +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 - -import java.util.concurrent.atomic.{AtomicLong, AtomicReference} - -import fr.acinq.eclair.blockchain.fee.{FeeratesPerKB, FeeratesPerKw} - -/** - * Created by PM on 25/01/2016. - */ -object Globals { - - /** - * This counter holds the current blockchain height. - * It is mainly used to calculate htlc expiries. - * The value is read by all actors, hence it needs to be thread-safe. - */ - val blockCount = new AtomicLong(0) - - /** - * This holds the current feerates, in satoshi-per-kilobytes. - * The value is read by all actors, hence it needs to be thread-safe. - */ - val feeratesPerKB = new AtomicReference[FeeratesPerKB](null) - - /** - * This holds the current feerates, in satoshi-per-kw. - * The value is read by all actors, hence it needs to be thread-safe. - */ - val feeratesPerKw = new AtomicReference[FeeratesPerKw](null) -} - - diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/JsonSerializers.scala index 361afefc93..1f8dfd8514 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/JsonSerializers.scala @@ -41,7 +41,10 @@ object JsonSerializers { implicit val directedHtlcReadWriter: ReadWriter[DirectedHtlc] = macroRW implicit val commitmentSpecReadWriter: ReadWriter[CommitmentSpec] = macroRW implicit val localChangesReadWriter: ReadWriter[LocalChanges] = macroRW - implicit val satoshiReadWriter: ReadWriter[Satoshi] = macroRW + implicit val cltvExpiryReadWriter: ReadWriter[CltvExpiry] = readwriter[Long].bimap(_.toLong, CltvExpiry.apply) + implicit val cltvExpiryDeltaReadWriter: ReadWriter[CltvExpiryDelta] = readwriter[Int].bimap(_.toInt, CltvExpiryDelta.apply) + implicit val millisatoshiReadWriter: ReadWriter[MilliSatoshi] = readwriter[Long].bimap(_.toLong, MilliSatoshi.apply) + implicit val satoshiReadWriter: ReadWriter[Satoshi] = readwriter[Long].bimap(_.toLong, Satoshi.apply) implicit val txOutReadWriter: ReadWriter[TxOut] = macroRW implicit val inputInfoReadWriter: ReadWriter[InputInfo] = macroRW implicit val transactionWithInputInfoReadWriter: ReadWriter[TransactionWithInputInfo] = ReadWriter.merge( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala new file mode 100644 index 0000000000..ff97d45565 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala @@ -0,0 +1,71 @@ +/* + * 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 + +import fr.acinq.bitcoin.{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 + */ +case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatoshi] { + + // @formatter:off + def +(other: MilliSatoshi) = MilliSatoshi(underlying + other.underlying) + def +(other: BtcAmount) = MilliSatoshi(underlying + other.toMilliSatoshi.underlying) + def -(other: MilliSatoshi) = MilliSatoshi(underlying - other.underlying) + def -(other: BtcAmount) = MilliSatoshi(underlying - other.toMilliSatoshi.underlying) + def *(m: Long) = MilliSatoshi(underlying * m) + def *(m: Double) = MilliSatoshi((underlying * m).toLong) + def /(d: Long) = MilliSatoshi(underlying / d) + def unary_-() = MilliSatoshi(-underlying) + + override def compare(other: MilliSatoshi): Int = underlying.compareTo(other.underlying) + // Since BtcAmount is a sealed trait that MilliSatoshi cannot extend, we need to redefine comparison operators. + def compare(other: BtcAmount): Int = compare(other.toMilliSatoshi) + def <=(other: BtcAmount): Boolean = compare(other) <= 0 + def >=(other: BtcAmount): Boolean = compare(other) >= 0 + def <(other: BtcAmount): Boolean = compare(other) < 0 + def >(other: BtcAmount): Boolean = compare(other) > 0 + + // We provide asymmetric min/max functions to provide more control on the return type. + def max(other: MilliSatoshi): MilliSatoshi = if (this > other) this else other + def max(other: BtcAmount): MilliSatoshi = if (this > other) this else other.toMilliSatoshi + def min(other: MilliSatoshi): MilliSatoshi = if (this < other) this else other + def min(other: BtcAmount): MilliSatoshi = if (this < other) this else other.toMilliSatoshi + + def truncateToSatoshi: Satoshi = Satoshi(underlying / 1000) + def toLong: Long = underlying + override def toString = s"$underlying msat" + // @formatter:on + +} + +object MilliSatoshi { + + private def satoshi2millisatoshi(input: Satoshi): MilliSatoshi = MilliSatoshi(input.toLong * 1000L) + + def toMilliSatoshi(amount: BtcAmount): MilliSatoshi = amount match { + case sat: Satoshi => satoshi2millisatoshi(sat) + case millis: MilliBtc => satoshi2millisatoshi(millibtc2satoshi(millis)) + case bitcoin: Btc => satoshi2millisatoshi(btc2satoshi(bitcoin)) + } + +} 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 f6e14e7ed9..6ae7cdc1ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -20,45 +20,48 @@ import java.io.File import java.net.InetSocketAddress import java.sql.DriverManager import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong import com.google.common.io.Files import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32} -import fr.acinq.eclair.NodeParams.{WatcherType} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} +import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, OnChainFeeConf} import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.wire.{Color, NodeAddress} +import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress} import scodec.bits.ByteVector import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration /** - * Created by PM on 26/02/2017. - */ + * Created by PM on 26/02/2017. + */ case class NodeParams(keyManager: KeyManager, + private val blockCount: AtomicLong, alias: String, color: Color, publicAddresses: List[NodeAddress], globalFeatures: ByteVector, localFeatures: ByteVector, overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)], - dustLimitSatoshis: Long, + syncWhitelist: Set[PublicKey], + dustLimit: Satoshi, onChainFeeConf: OnChainFeeConf, maxHtlcValueInFlightMsat: UInt64, maxAcceptedHtlcs: Int, - expiryDeltaBlocks: Int, - fulfillSafetyBeforeTimeoutBlocks: Int, - htlcMinimumMsat: Int, - toRemoteDelayBlocks: Int, - maxToLocalDelayBlocks: Int, + expiryDeltaBlocks: CltvExpiryDelta, + fulfillSafetyBeforeTimeoutBlocks: CltvExpiryDelta, + htlcMinimum: MilliSatoshi, + toRemoteDelayBlocks: CltvExpiryDelta, + maxToLocalDelayBlocks: CltvExpiryDelta, minDepthBlocks: Int, - feeBaseMsat: Int, + feeBase: MilliSatoshi, feeProportionalMillionth: Int, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double, @@ -74,13 +77,13 @@ case class NodeParams(keyManager: KeyManager, channelFlags: Byte, watcherType: WatcherType, paymentRequestExpiry: FiniteDuration, - minFundingSatoshis: Long, + minFundingSatoshis: Satoshi, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams], maxPaymentAttempts: Int) { - val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId + def currentBlockHeight: Long = blockCount.get } object NodeParams { @@ -92,17 +95,17 @@ object NodeParams { object ELECTRUM extends WatcherType /** - * Order of precedence for the configuration parameters: - * 1) Java environment variables (-D...) - * 2) Configuration file eclair.conf - * 3) Optionally provided config - * 4) Default values in reference.conf - */ + * Order of precedence for the configuration parameters: + * 1) Java environment variables (-D...) + * 2) Configuration file eclair.conf + * 3) Optionally provided config + * 4) Default values in reference.conf + */ def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) = ConfigFactory.parseProperties(System.getProperties) .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) .withFallback(overrideDefaults) - .withFallback(ConfigFactory.load()).getConfig("eclair") + .withFallback(ConfigFactory.load()) def getSeed(datadir: File): ByteVector = { val seedPath = new File(datadir, "seed.dat") @@ -125,7 +128,7 @@ object NodeParams { } } - def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases, feeEstimator: FeeEstimator): NodeParams = { + def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases, blockCount: AtomicLong, feeEstimator: FeeEstimator): NodeParams = { val chain = config.getString("chain") val chainHash = makeChainHash(chain) @@ -138,7 +141,7 @@ object NodeParams { case _ => BITCOIND } - val dustLimitSatoshis = config.getLong("dust-limit-satoshis") + val dustLimitSatoshis = Satoshi(config.getLong("dust-limit-satoshis")) if (chainHash == Block.LivenetGenesisBlock.hash) { require(dustLimitSatoshis >= Channel.MIN_DUSTLIMIT, s"dust limit must be greater than ${Channel.MIN_DUSTLIMIT}") } @@ -146,12 +149,12 @@ object NodeParams { 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 maxToLocalCLTV = config.getInt("max-to-local-delay-blocks") - val offeredCLTV = config.getInt("to-remote-delay-blocks") + val maxToLocalCLTV = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")) + val offeredCLTV = CltvExpiryDelta(config.getInt("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 expiryDeltaBlocks = config.getInt("expiry-delta-blocks") - val fulfillSafetyBeforeTimeoutBlocks = config.getInt("fulfill-safety-before-timeout-blocks") + val expiryDeltaBlocks = CltvExpiryDelta(config.getInt("expiry-delta-blocks")) + val fulfillSafetyBeforeTimeoutBlocks = CltvExpiryDelta(config.getInt("fulfill-safety-before-timeout-blocks")) require(fulfillSafetyBeforeTimeoutBlocks < expiryDeltaBlocks, "fulfill-safety-before-timeout-blocks must be smaller than expiry-delta-blocks") val nodeAlias = config.getString("node-alias") @@ -164,6 +167,8 @@ object NodeParams { p -> (gf, lf) }.toMap + val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").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")), @@ -188,30 +193,43 @@ object NodeParams { claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main") ) + val feeBase = MilliSatoshi(config.getInt("fee-base-msat")) + // fee base is in msat but is encoded on 32 bits and not 64 in the BOLTs, which is why it has + // to be below 0x100000000 msat which is about 42 mbtc + require(feeBase <= MilliSatoshi(0xFFFFFFFFL), "fee-base-msat must be below 42 mbtc") + + val routerSyncEncodingType = config.getString("router.sync.encoding-type") match { + case "uncompressed" => EncodingType.UNCOMPRESSED + case "zlib" => EncodingType.COMPRESSED_ZLIB + } + NodeParams( keyManager = keyManager, + blockCount = blockCount, alias = nodeAlias, color = Color(color(0), color(1), color(2)), publicAddresses = addresses, globalFeatures = ByteVector.fromValidHex(config.getString("global-features")), localFeatures = ByteVector.fromValidHex(config.getString("local-features")), overrideFeatures = overrideFeatures, - dustLimitSatoshis = dustLimitSatoshis, + syncWhitelist = syncWhitelist, + dustLimit = dustLimitSatoshis, onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, feeEstimator = feeEstimator, maxFeerateMismatch = config.getDouble("on-chain-fees.max-feerate-mismatch"), + closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio") ), maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")), maxAcceptedHtlcs = maxAcceptedHtlcs, expiryDeltaBlocks = expiryDeltaBlocks, fulfillSafetyBeforeTimeoutBlocks = fulfillSafetyBeforeTimeoutBlocks, - htlcMinimumMsat = config.getInt("htlc-minimum-msat"), - toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"), - maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"), + htlcMinimum = MilliSatoshi(config.getInt("htlc-minimum-msat")), + toRemoteDelayBlocks = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")), + maxToLocalDelayBlocks = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")), minDepthBlocks = config.getInt("mindepth-blocks"), - feeBaseMsat = config.getInt("fee-base-msat"), + feeBase = feeBase, feeProportionalMillionth = config.getInt("fee-proportional-millionths"), reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), @@ -227,14 +245,19 @@ object NodeParams { channelFlags = config.getInt("channel-flags").toByte, watcherType = watcherType, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry", TimeUnit.SECONDS), TimeUnit.SECONDS), - minFundingSatoshis = config.getLong("min-funding-satoshis"), + minFundingSatoshis = Satoshi(config.getLong("min-funding-satoshis")), routerConf = RouterConf( channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration", TimeUnit.SECONDS), TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval", TimeUnit.SECONDS), TimeUnit.SECONDS), + networkStatsRefreshInterval = FiniteDuration(config.getDuration("router.network-stats-interval", TimeUnit.SECONDS), TimeUnit.SECONDS), randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"), + requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), + encodingType = routerSyncEncodingType, + channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"), + channelQueryChunkSize = config.getInt("router.sync.channel-query-chunk-size"), searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"), - searchMaxCltv = config.getInt("router.path-finding.max-cltv"), - searchMaxFeeBaseSat = config.getLong("router.path-finding.fee-threshold-sat"), + searchMaxCltv = CltvExpiryDelta(config.getInt("router.path-finding.max-cltv")), + searchMaxFeeBase = Satoshi(config.getLong("router.path-finding.fee-threshold-sat")), searchMaxFeePct = config.getDouble("router.path-finding.max-fee-pct"), searchHeuristicsEnabled = config.getBoolean("router.path-finding.heuristics-enable"), searchRatioCltv = config.getDouble("router.path-finding.ratio-cltv"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala new file mode 100644 index 0000000000..47d083d9d7 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala @@ -0,0 +1,41 @@ +/* + * 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 + +import kamon.Kamon + +import scala.concurrent.{ExecutionContext, Future} + +object KamonExt { + + def time[T](name: String)(f: => T) = { + val timer = Kamon.timer(name).withoutTags().start() + try { + f + } finally { + timer.stop() + } + } + + def timeFuture[T](name: String)(f: => Future[T])(implicit ec: ExecutionContext): Future[T] = { + val timer = Kamon.timer(name).withoutTags().start() + val res = f + res onComplete { case _ => timer.stop } + res + } + +} 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 4d9f7c9de3..0b464cd0bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -19,13 +19,15 @@ package fr.acinq.eclair import java.io.File import java.net.InetSocketAddress import java.sql.DriverManager +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} import akka.Done import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy} import akka.util.Timeout import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.NodeParams.ELECTRUM import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL @@ -60,6 +62,8 @@ class Setup(datadir: File, seed_opt: Option[ByteVector] = None, db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging { + implicit val timeout = Timeout(30 seconds) + implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global implicit val sttpBackend = OkHttpFutureBackend() @@ -69,7 +73,8 @@ class Setup(datadir: File, datadir.mkdirs() - val config = NodeParams.loadConfiguration(datadir, overrideDefaults) + val appConfig = NodeParams.loadConfiguration(datadir, overrideDefaults) + val config = appConfig.getConfig("eclair") val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) val chain = config.getString("chain") val chaindir = new File(datadir, chain) @@ -80,12 +85,31 @@ class Setup(datadir: File, case None => Databases.sqliteJDBC(chaindir) } + /** + * This counter holds the current blockchain height. + * It is mainly used to calculate htlc expiries. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val blockCount = new AtomicLong(0) + + /** + * This holds the current feerates, in satoshi-per-kilobytes. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKB = new AtomicReference[FeeratesPerKB](null) + + /** + * This holds the current feerates, in satoshi-per-kw. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKw = new AtomicReference[FeeratesPerKw](null) + val feeEstimator = new FeeEstimator { - override def getFeeratePerKb(target: Int): Long = Globals.feeratesPerKB.get().feePerBlock(target) - override def getFeeratePerKw(target: Int): Long = Globals.feeratesPerKw.get().feePerBlock(target) + override def getFeeratePerKb(target: Int): Long = feeratesPerKB.get().feePerBlock(target) + override def getFeeratePerKw(target: Int): Long = feeratesPerKw.get().feePerBlock(target) } - val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, feeEstimator) + val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, blockCount, feeEstimator) val serverBindingAddress = new InetSocketAddress( config.getString("server.binding-ip"), @@ -127,7 +151,7 @@ class Setup(datadir: File, val stream = classOf[Setup].getResourceAsStream(addressesFile) ElectrumClientPool.readServerAddresses(stream, sslEnabled) } - val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(addresses)), "electrum-client", SupervisorStrategy.Resume)) + val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(blockCount, addresses)), "electrum-client", SupervisorStrategy.Resume)) Electrum(electrumClient) case _ => ??? } @@ -142,32 +166,30 @@ class Setup(datadir: File, blocks_72 = config.getLong("on-chain-fees.default-feerates.72"), blocks_144 = config.getLong("on-chain-fees.default-feerates.144") ) - Globals.feeratesPerKB.set(confDefaultFeerates) - Globals.feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates)) + feeratesPerKB.set(confDefaultFeerates) + feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates)) confDefaultFeerates } minFeeratePerByte = config.getLong("min-feerate") smoothFeerateWindow = config.getInt("smooth-feerate-window") feeProvider = (nodeParams.chainHash, bitcoin) match { case (Block.RegtestGenesisBlock.hash, _) => new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) - case (_, Bitcoind(bitcoinClient)) => - new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! case _ => new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! } _ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map { case feerates: FeeratesPerKB => - Globals.feeratesPerKB.set(feerates) - Globals.feeratesPerKw.set(FeeratesPerKw(feerates)) - system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get)) - logger.info(s"current feeratesPerKB=${Globals.feeratesPerKB.get()} feeratesPerKw=${Globals.feeratesPerKw.get()}") + feeratesPerKB.set(feerates) + feeratesPerKw.set(FeeratesPerKw(feerates)) + system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) + logger.info(s"current feeratesPerKB=${feeratesPerKB.get()} feeratesPerKw=${feeratesPerKw.get()}") feeratesRetrieved.trySuccess(Done) }) _ <- feeratesRetrieved.future watcher = bitcoin match { case Electrum(electrumClient) => - system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume)) + system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(blockCount, electrumClient)), "watcher", SupervisorStrategy.Resume)) case _ => ??? } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SyncLiteSetup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SyncLiteSetup.scala index 0cf8538cf5..edd135e6fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SyncLiteSetup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SyncLiteSetup.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair import java.io.File +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} import akka.actor.{Actor, ActorLogging, ActorSystem, Props, ReceiveTimeout, SupervisorStrategy} import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Satoshi, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.fee.FeeEstimator +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKB, FeeratesPerKw} import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db.Databases @@ -56,11 +57,31 @@ class SyncLiteSetup(datadir: File, case Some(d) => d case None => Databases.sqliteJDBC(new File(datadir, chain)) } + /** + * This counter holds the current blockchain height. + * It is mainly used to calculate htlc expiries. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val blockCount = new AtomicLong(0) + + /** + * This holds the current feerates, in satoshi-per-kilobytes. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKB = new AtomicReference[FeeratesPerKB](null) + + /** + * This holds the current feerates, in satoshi-per-kw. + * The value is read by all actors, hence it needs to be thread-safe. + */ + val feeratesPerKw = new AtomicReference[FeeratesPerKw](null) + val feeEstimator = new FeeEstimator { - override def getFeeratePerKb(target: Int): Long = Globals.feeratesPerKB.get().feePerBlock(target) - override def getFeeratePerKw(target: Int): Long = Globals.feeratesPerKw.get().feePerBlock(target) + override def getFeeratePerKb(target: Int): Long = feeratesPerKB.get().feePerBlock(target) + override def getFeeratePerKw(target: Int): Long = feeratesPerKw.get().feePerBlock(target) } - val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, feeEstimator) + + val nodeParams = NodeParams.makeNodeParams(config, keyManager, None, database, blockCount, feeEstimator) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/UInt64.scala b/eclair-core/src/main/scala/fr/acinq/eclair/UInt64.scala index c643c956ae..5a05c3bf43 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/UInt64.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/UInt64.scala @@ -16,34 +16,35 @@ package fr.acinq.eclair -import java.math.BigInteger - +import com.google.common.primitives.UnsignedLongs import scodec.bits.ByteVector +import scodec.bits.HexStringSyntax -case class UInt64(private val underlying: BigInt) extends Ordered[UInt64] { +case class UInt64(private val underlying: Long) extends Ordered[UInt64] { - require(underlying >= 0, s"uint64 must be positive (actual=$underlying)") - require(underlying <= UInt64.MaxValueBigInt, s"uint64 must be < 2^64 -1 (actual=$underlying)") + override def compare(o: UInt64): Int = UnsignedLongs.compare(underlying, o.underlying) + private def compare(other: MilliSatoshi): Int = other.toLong match { + case l if l < 0 => 1 // if @param 'other' is negative then is always smaller than 'this' + case _ => compare(UInt64(other.toLong)) // we must do an unsigned comparison here because the uint64 can exceed the capacity of MilliSatoshi class + } - override def compare(o: UInt64): Int = underlying.compare(o.underlying) + def <(other: MilliSatoshi): Boolean = compare(other) < 0 + def >(other: MilliSatoshi): Boolean = compare(other) > 0 + def <=(other: MilliSatoshi): Boolean = compare(other) <= 0 + def >=(other: MilliSatoshi): Boolean = compare(other) >= 0 - def toByteVector: ByteVector = ByteVector.view(underlying.toByteArray.takeRight(8)) + def toByteVector: ByteVector = ByteVector.fromLong(underlying) - def toBigInt: BigInt = underlying + def toBigInt: BigInt = (BigInt(underlying >>> 1) << 1) + (underlying & 1) - override def toString: String = underlying.toString + override def toString: String = UnsignedLongs.toString(underlying, 10) } - object UInt64 { - private val MaxValueBigInt = BigInt(new BigInteger("ffffffffffffffff", 16)) + val MaxValue = UInt64(hex"0xffffffffffffffff") - val MaxValue = UInt64(MaxValueBigInt) - - def apply(bin: ByteVector) = new UInt64(new BigInteger(1, bin.toArray)) - - def apply(value: Long) = new UInt64(BigInt(value)) + def apply(bin: ByteVector): UInt64 = UInt64(bin.toLong(signed = false)) object Conversions { @@ -51,5 +52,4 @@ object UInt64 { implicit def longToUint64(l: Long) = UInt64(l) } - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index f32856491e..efdd802f69 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -122,7 +122,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC exists <- getTransaction(tx.txid) .map(_ => true) // we have found the transaction .recover { - case JsonRPCError(Error(_, message)) if message.contains("indexing") => + case JsonRPCError(Error(_, message)) if message.contains("index") => sys.error("Fatal error: bitcoind is indexing!!") System.exit(1) // bitcoind is indexing, that's a fatal error!! false // won't be reached 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 b6feb4afff..73511db293 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 @@ -17,11 +17,11 @@ package fr.acinq.eclair.blockchain.bitcoind import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicLong import akka.actor.{Actor, ActorLogging, Cancellable, Props, Terminated} import akka.pattern.pipe import fr.acinq.bitcoin._ -import fr.acinq.eclair.Globals import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED @@ -39,7 +39,7 @@ import scala.util.Try * - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs) * Created by PM on 21/02/2016. */ -class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { +class ZmqWatcher(blockCount: AtomicLong, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { import ZmqWatcher._ @@ -80,7 +80,7 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = client.getBlockCount.map { case count => log.debug(s"setting blockCount=$count") - Globals.blockCount.set(count) + blockCount.set(count) context.system.eventStream.publish(CurrentBlockCount(count)) } // TODO: beware of the herd effect @@ -151,7 +151,7 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = context become watching(watches + w, addWatchedUtxos(watchedUtxos, w), block2tx, nextTick) case PublishAsap(tx) => - val blockCount = Globals.blockCount.get() + val blockCount = this.blockCount.get() val cltvTimeout = Scripts.cltvTimeout(tx) val csvTimeout = Scripts.csvTimeout(tx) if (csvTimeout > 0) { @@ -168,7 +168,7 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _, _) => log.info(s"parent tx of txid=${tx.txid} has been confirmed") - val blockCount = Globals.blockCount.get() + val blockCount = this.blockCount.get() val csvTimeout = Scripts.csvTimeout(tx) val absTimeout = blockHeight + csvTimeout if (absTimeout > blockCount) { @@ -226,7 +226,7 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = object ZmqWatcher { - def props(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(client)(ec)) + def props(blockCount: AtomicLong, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(blockCount, client)(ec)) case object TickNewBlock diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala index c7b7e492da..ff277febca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala @@ -21,6 +21,7 @@ import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.TxCoordinates import fr.acinq.eclair.blockchain.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} import fr.acinq.eclair.wire.ChannelAnnouncement +import kamon.Kamon import org.json4s.JsonAST._ import scala.concurrent.{ExecutionContext, Future} @@ -149,26 +150,36 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) { def validate(c: ChannelAnnouncement)(implicit ec: ExecutionContext): Future[ValidateResult] = { val TxCoordinates(blockHeight, txIndex, outputIndex) = coordinates(c.shortChannelId) - - for { - blockHash: String <- rpcClient.invoke("getblockhash", blockHeight).map(_.extractOrElse[String](ByteVector32.Zeroes.toHex)) - txid: String <- rpcClient.invoke("getblock", blockHash).map { - case json => Try { - val JArray(txs) = json \ "tx" - txs(txIndex).extract[String] - } getOrElse ByteVector32.Zeroes.toHex - } - tx <- getRawTransaction(txid) - unspent <- isTransactionOutputSpendable(txid, outputIndex, includeMempool = true) - fundingTxStatus <- if (unspent) { - Future.successful(UtxoStatus.Unspent) - } else { - // if this returns true, it means that the spending tx is *not* in the blockchain - isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map { - case res => UtxoStatus.Spent(spendingTxConfirmed = !res) + val span = Kamon.spanBuilder("validate-bitcoin-client").start() + for { + _ <- Future.successful(0) + span0 = Kamon.spanBuilder("getblockhash").start() + blockHash: String <- rpcClient.invoke("getblockhash", blockHeight).map(_.extractOrElse[String](ByteVector32.Zeroes.toHex)) + _ = span0.finish() + span1 = Kamon.spanBuilder("getblock").start() + txid: String <- rpcClient.invoke("getblock", blockHash).map { + case json => Try { + val JArray(txs) = json \ "tx" + txs(txIndex).extract[String] + } getOrElse ByteVector32.Zeroes.toHex } - } - } yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus))) + _ = span1.finish() + span2 = Kamon.spanBuilder("getrawtx").start() + tx <- getRawTransaction(txid) + _ = span2.finish() + span3 = Kamon.spanBuilder("utxospendable-mempool").start() + unspent <- isTransactionOutputSpendable(txid, outputIndex, includeMempool = true) + _ = span3.finish() + fundingTxStatus <- if (unspent) { + Future.successful(UtxoStatus.Unspent) + } else { + // if this returns true, it means that the spending tx is *not* in the blockchain + isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map { + case res => UtxoStatus.Spent(spendingTxConfirmed = !res) + } + } + _ = span.finish() + } yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus))) } recover { case t: Throwable => ValidateResult(c, Left(t)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala index b60662583d..2da34e01f0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala @@ -67,7 +67,12 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec case SSL.OFF => () case SSL.STRICT => val sslCtx = SslContextBuilder.forClient.build - ch.pipeline.addLast(sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort)) + val handler = sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort) + val sslParameters = handler.engine().getSSLParameters + sslParameters.setEndpointIdentificationAlgorithm("HTTPS") + handler.engine().setSSLParameters(sslParameters) + handler.engine().setEnabledProtocols(Array[String]("TLSv1.2", "TLSv1.3")) + ch.pipeline.addLast(handler) case SSL.LOOSE => // INSECURE VERSION THAT DOESN'T CHECK CERTIFICATE val sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala index b7249d409a..117b204bf1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala @@ -18,10 +18,10 @@ package fr.acinq.eclair.blockchain.electrum import java.io.InputStream import java.net.InetSocketAddress +import java.util.concurrent.atomic.AtomicLong import akka.actor.{Actor, ActorRef, FSM, OneForOneStrategy, Props, SupervisorStrategy, Terminated} import fr.acinq.bitcoin.BlockHeader -import fr.acinq.eclair.Globals import fr.acinq.eclair.blockchain.CurrentBlockCount import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress @@ -32,7 +32,7 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration._ import scala.util.Random -class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit val ec: ExecutionContext) extends Actor with FSM[ElectrumClientPool.State, ElectrumClientPool.Data] { +class ElectrumClientPool(blockCount: AtomicLong, serverAddresses: Set[ElectrumServerAddress])(implicit val ec: ExecutionContext) extends Actor with FSM[ElectrumClientPool.State, ElectrumClientPool.Data] { import ElectrumClientPool._ val statusListeners = collection.mutable.HashSet.empty[ActorRef] @@ -166,10 +166,10 @@ class ElectrumClientPool(serverAddresses: Set[ElectrumServerAddress])(implicit v private def updateBlockCount(blockCount: Long): Unit = { // when synchronizing we don't want to advertise previous blocks - if (Globals.blockCount.get() < blockCount) { + if (this.blockCount.get() < blockCount) { log.debug("current blockchain height={}", blockCount) context.system.eventStream.publish(CurrentBlockCount(blockCount)) - Globals.blockCount.set(blockCount) + this.blockCount.set(blockCount) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala index 428623f9e3..2f767107f8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala @@ -20,6 +20,7 @@ import akka.actor.{ActorRef, FSM, PoisonPill, Props} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey, hardened} import fr.acinq.bitcoin.{Base58, Base58Check, Block, ByteVector32, Crypto, DeterministicWallet, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptElt, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.eclair.LongToBtcAmount import fr.acinq.eclair.blockchain.bitcoind.rpc.Error import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ import fr.acinq.eclair.blockchain.electrum.db.{HeaderDb, WalletDb} @@ -31,21 +32,21 @@ import scala.annotation.tailrec import scala.util.{Failure, Success, Try} /** - * Simple electrum wallet - * - * Typical workflow: - * - * client ---- header update ----> wallet - * client ---- status update ----> wallet - * client <--- ask history ----- wallet - * client ---- history ----> wallet - * client <--- ask tx ----- wallet - * client ---- tx ----> wallet - * - * @param seed - * @param client - * @param params - */ + * Simple electrum wallet + * + * Typical workflow: + * + * client ---- header update ----> wallet + * client ---- status update ----> wallet + * client <--- ask history ----- wallet + * client ---- history ----> wallet + * client <--- ask tx ----- wallet + * client ---- tx ----> wallet + * + * @param seed + * @param client + * @param params + */ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.WalletParameters) extends FSM[ElectrumWallet.State, ElectrumWallet.Data] { import Blockchain.RETARGETING_PERIOD @@ -65,13 +66,13 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet. // +--------------------------------------------+ /** - * If the wallet is ready and its state changed since the last time it was ready: - * - publish a `WalletReady` notification - * - persist state data - * - * @param data wallet data - * @return the input data with an updated 'last ready message' if needed - */ + * If the wallet is ready and its state changed since the last time it was ready: + * - publish a `WalletReady` notification + * - persist state data + * + * @param data wallet data + * @return the input data with an updated 'last ready message' if needed + */ def persistAndNotify(data: ElectrumWallet.Data): ElectrumWallet.Data = { if (data.isReady(swipeRange)) { data.lastReadyMessage match { @@ -289,11 +290,11 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet. pendingHeadersRequests1 ++= data.pendingHeadersRequests /** - * If we don't already have a header at this height, or a pending request to download the header chunk it's in, - * download this header chunk. - * We don't have this header because it's most likely older than our current checkpoint, downloading the whole header - * chunk (2016 headers) is quick and they're easy to verify. - */ + * If we don't already have a header at this height, or a pending request to download the header chunk it's in, + * download this header chunk. + * We don't have this header because it's most likely older than our current checkpoint, downloading the whole header + * chunk (2016 headers) is quick and they're easy to verify. + */ def downloadHeadersIfMissing(height: Int): Unit = { if (data.blockchain.getHeader(height).orElse(params.walletDb.getHeader(height)).isEmpty) { // we don't have this header, probably because it is older than our checkpoints @@ -423,7 +424,7 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet. case Event(CompleteTransaction(tx, feeRatePerKw), data) => Try(data.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, allowSpendUnconfirmed)) match { case Success((data1, tx1, fee1)) => stay using data1 replying CompleteTransactionResponse(tx1, fee1, None) - case Failure(t) => stay replying CompleteTransactionResponse(tx, Satoshi(0), Some(t)) + case Failure(t) => stay replying CompleteTransactionResponse(tx, 0 sat, Some(t)) } case Event(SendAll(publicKeyScript, feeRatePerKw), data) => @@ -495,7 +496,7 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet. object ElectrumWallet { def props(seed: ByteVector, client: ActorRef, params: WalletParameters): Props = Props(new ElectrumWallet(seed, client, params)) - case class WalletParameters(chainHash: ByteVector32, walletDb: WalletDb, minimumFee: Satoshi = Satoshi(2000), dustLimit: Satoshi = Satoshi(546), swipeRange: Int = 10, allowSpendUnconfirmed: Boolean = true) + case class WalletParameters(chainHash: ByteVector32, walletDb: WalletDb, minimumFee: Satoshi = 2000 sat, dustLimit: Satoshi = 546 sat, swipeRange: Int = 10, allowSpendUnconfirmed: Boolean = true) // @formatter:off sealed trait State @@ -559,10 +560,10 @@ object ElectrumWallet { // @formatter:on /** - * - * @param key public key - * @return the address of the p2sh-of-p2wpkh script for this key - */ + * + * @param key public key + * @return the address of the p2sh-of-p2wpkh script for this key + */ def segwitAddress(key: PublicKey, chainHash: ByteVector32): String = { val script = Script.pay2wpkh(key) val hash = Crypto.hash160(Script.write(script)) @@ -577,17 +578,17 @@ object ElectrumWallet { def segwitAddress(key: PrivateKey, chainHash: ByteVector32): String = segwitAddress(key.publicKey, chainHash) /** - * - * @param key public key - * @return a p2sh-of-p2wpkh script for this key - */ + * + * @param key public key + * @return a p2sh-of-p2wpkh script for this key + */ def computePublicKeyScript(key: PublicKey) = Script.pay2sh(Script.pay2wpkh(key)) /** - * - * @param key public key - * @return the hash of the public key script for this key, as used by Electrum's hash-based methods - */ + * + * @param key public key + * @return the hash of the public key script for this key, as used by Electrum's hash-based methods + */ def computeScriptHashFromPublicKey(key: PublicKey): ByteVector32 = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse def accountPath(chainHash: ByteVector32): List[Long] = chainHash match { @@ -596,21 +597,21 @@ object ElectrumWallet { } /** - * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh - * - * @param master master key - * @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet - */ + * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh + * + * @param master master key + * @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet + */ def accountKey(master: ExtendedPrivateKey, chainHash: ByteVector32) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 0L :: Nil) /** - * Compute the wallet's xpub - * - * @param master master key - * @param chainHash chain hash - * @return a (xpub, path) tuple where xpub is the encoded account public key, and path is the derivation path for the account key - */ + * Compute the wallet's xpub + * + * @param master master key + * @param chainHash chain hash + * @return a (xpub, path) tuple where xpub is the encoded account public key, and path is the derivation path for the account key + */ def computeXpub(master: ExtendedPrivateKey, chainHash: ByteVector32): (String, String) = { val xpub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, accountPath(chainHash))) val prefix = chainHash match { @@ -621,11 +622,11 @@ object ElectrumWallet { } /** - * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh - * - * @param master master key - * @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet - */ + * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh + * + * @param master master key + * @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet + */ def changeKey(master: ExtendedPrivateKey, chainHash: ByteVector32) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 1L :: Nil) def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum) @@ -633,18 +634,18 @@ object ElectrumWallet { def totalAmount(utxos: Set[Utxo]): Satoshi = totalAmount(utxos.toSeq) /** - * - * @param weight transaction weight - * @param feeRatePerKw fee rate - * @return the fee for this tx weight - */ + * + * @param weight transaction weight + * @param feeRatePerKw fee rate + * @return the fee for this tx weight + */ def computeFee(weight: Int, feeRatePerKw: Long): Satoshi = Satoshi((weight * feeRatePerKw) / 1000) /** - * - * @param txIn transaction input - * @return Some(pubkey) if this tx input spends a p2sh-of-p2wpkh(pub), None otherwise - */ + * + * @param txIn transaction input + * @return Some(pubkey) if this tx input spends a p2sh-of-p2wpkh(pub), None otherwise + */ def extractPubKeySpentFrom(txIn: TxIn): Option[PublicKey] = { Try { // we're looking for tx that spend a pay2sh-of-p2wkph output @@ -672,25 +673,25 @@ object ElectrumWallet { } /** - * Wallet state, which stores data returned by Electrum servers. - * Most items are indexed by script hash (i.e. by pubkey script sha256 hash). - * Height follows Electrum's conventions: - * - h > 0 means that the tx was confirmed at block #h - * - 0 means unconfirmed, but all input are confirmed - * < 0 means unconfirmed, and some inputs are unconfirmed as well - * - * @param blockchain blockchain - * @param accountKeys account keys - * @param changeKeys change keys - * @param status script hash -> status; "" means that the script hash has not been used yet - * @param transactions wallet transactions - * @param heights transactions heights - * @param history script hash -> history - * @param locks transactions which lock some of our utxos. - * @param pendingHistoryRequests requests pending a response from the electrum server - * @param pendingTransactionRequests requests pending a response from the electrum server - * @param pendingTransactions transactions received but not yet connected to their parents - */ + * Wallet state, which stores data returned by Electrum servers. + * Most items are indexed by script hash (i.e. by pubkey script sha256 hash). + * Height follows Electrum's conventions: + * - h > 0 means that the tx was confirmed at block #h + * - 0 means unconfirmed, but all input are confirmed + * < 0 means unconfirmed, and some inputs are unconfirmed as well + * + * @param blockchain blockchain + * @param accountKeys account keys + * @param changeKeys change keys + * @param status script hash -> status; "" means that the script hash has not been used yet + * @param transactions wallet transactions + * @param heights transactions heights + * @param history script hash -> history + * @param locks transactions which lock some of our utxos. + * @param pendingHistoryRequests requests pending a response from the electrum server + * @param pendingTransactionRequests requests pending a response from the electrum server + * @param pendingTransactions transactions received but not yet connected to their parents + */ case class Data(blockchain: Blockchain, accountKeys: Vector[ExtendedPrivateKey], changeKeys: Vector[ExtendedPrivateKey], @@ -720,10 +721,10 @@ object ElectrumWallet { lazy val utxos = history.keys.toSeq.map(scriptHash => getUtxos(scriptHash)).flatten /** - * The wallet is ready if all current keys have an empty status, and we don't have - * any history/tx request pending - * NB: swipeRange * 2 because we have account keys and change keys - */ + * The wallet is ready if all current keys have an empty status, and we don't have + * any history/tx request pending + * NB: swipeRange * 2 because we have account keys and change keys + */ def isReady(swipeRange: Int) = status.filter(_._2 == "").size >= swipeRange * 2 && pendingHistoryRequests.isEmpty && pendingTransactionRequests.isEmpty def readyMessage: WalletReady = { @@ -732,22 +733,22 @@ object ElectrumWallet { } /** - * - * @return the ids of transactions that belong to our wallet history for this script hash but that we don't have - * and have no pending requests for. - */ + * + * @return the ids of transactions that belong to our wallet history for this script hash but that we don't have + * and have no pending requests for. + */ def missingTransactions(scriptHash: ByteVector32): Set[ByteVector32] = { val txids = history.getOrElse(scriptHash, List()).map(_.tx_hash).filterNot(txhash => transactions.contains(txhash)).toSet txids -- pendingTransactionRequests } /** - * - * @return the current receive key. In most cases it will be a key that has not - * been used yet but it may be possible that we are still looking for - * unused keys and none is available yet. In this case we will return - * the latest account key. - */ + * + * @return the current receive key. In most cases it will be a key that has not + * been used yet but it may be possible that we are still looking for + * unused keys and none is available yet. In this case we will return + * the latest account key. + */ def currentReceiveKey = firstUnusedAccountKeys.headOption.getOrElse { // bad luck we are still looking for unused keys // use the first account key @@ -757,12 +758,12 @@ object ElectrumWallet { def currentReceiveAddress = segwitAddress(currentReceiveKey, chainHash) /** - * - * @return the current change key. In most cases it will be a key that has not - * been used yet but it may be possible that we are still looking for - * unused keys and none is available yet. In this case we will return - * the latest change key. - */ + * + * @return the current change key. In most cases it will be a key that has not + * been used yet but it may be possible that we are still looking for + * unused keys and none is available yet. In this case we will return + * the latest change key. + */ def currentChangeKey = firstUnusedChangeKeys.headOption.getOrElse { // bad luck we are still looking for unused keys // use the first account key @@ -776,11 +777,11 @@ object ElectrumWallet { def isSpend(txIn: TxIn, publicKey: PublicKey): Boolean = extractPubKeySpentFrom(txIn).contains(publicKey) /** - * - * @param txIn - * @param scriptHash - * @return true if txIn spends from an address that matches scriptHash - */ + * + * @param txIn + * @param scriptHash + * @return true if txIn spends from an address that matches scriptHash + */ def isSpend(txIn: TxIn, scriptHash: ByteVector32): Boolean = extractPubKeySpentFrom(txIn).exists(pub => computeScriptHashFromPublicKey(pub) == scriptHash) def isReceive(txOut: TxOut, scriptHash: ByteVector32): Boolean = publicScriptMap.get(txOut.publicKeyScript).exists(key => computeScriptHashFromPublicKey(key.publicKey) == scriptHash) @@ -790,11 +791,11 @@ object ElectrumWallet { def computeTransactionDepth(txid: ByteVector32): Long = heights.get(txid).map(height => if (height > 0) computeDepth(blockchain.tip.height, height) else 0).getOrElse(0) /** - * - * @param txid transaction id - * @param headerDb header db - * @return the timestamp of the block this tx was included in - */ + * + * @param txid transaction id + * @param headerDb header db + * @return the timestamp of the block this tx was included in + */ def computeTimestamp(txid: ByteVector32, headerDb: HeaderDb): Option[Long] = { for { height <- heights.get(txid) @@ -803,10 +804,10 @@ object ElectrumWallet { } /** - * - * @param scriptHash script hash - * @return the list of UTXOs for this script hash (including unconfirmed UTXOs) - */ + * + * @param scriptHash script hash + * @return the list of UTXOs for this script hash (including unconfirmed UTXOs) + */ def getUtxos(scriptHash: ByteVector32) = { history.get(scriptHash) match { case None => Seq() @@ -834,16 +835,16 @@ object ElectrumWallet { /** - * - * @param scriptHash script hash - * @return the (confirmed, unconfirmed) balance for this script hash. This balance may not - * be up-to-date if we have not received all data we've asked for yet. - */ + * + * @param scriptHash script hash + * @return the (confirmed, unconfirmed) balance for this script hash. This balance may not + * be up-to-date if we have not received all data we've asked for yet. + */ def balance(scriptHash: ByteVector32): (Satoshi, Satoshi) = { history.get(scriptHash) match { - case None => (Satoshi(0), Satoshi(0)) + case None => (0 sat, 0 sat) - case Some(items) if items.isEmpty => (Satoshi(0), Satoshi(0)) + case Some(items) if items.isEmpty => (0 sat, 0 sat) case Some(items) => val (confirmedItems, unconfirmedItems) = items.partition(_.height > 0) @@ -852,16 +853,16 @@ object ElectrumWallet { if (confirmedTxs.size + unconfirmedTxs.size < confirmedItems.size + unconfirmedItems.size) logger.warn(s"we have not received all transactions yet, balance will not be up to date") def findOurSpentOutputs(txs: Seq[Transaction]): Seq[TxOut] = { - val inputs = txs.map(_.txIn).flatten.filter(txIn => isSpend(txIn, scriptHash)) - val spentOutputs = inputs.map(_.outPoint).map(outPoint => transactions.get(outPoint.txid).map(_.txOut(outPoint.index.toInt))).flatten + val inputs = txs.flatMap(_.txIn).filter(txIn => isSpend(txIn, scriptHash)) + val spentOutputs = inputs.map(_.outPoint).flatMap(outPoint => transactions.get(outPoint.txid).map(_.txOut(outPoint.index.toInt))) spentOutputs } val confirmedSpents = findOurSpentOutputs(confirmedTxs) - val confirmedReceived = confirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash)) + val confirmedReceived = confirmedTxs.flatMap(_.txOut).filter(txOut => isReceive(txOut, scriptHash)) val unconfirmedSpents = findOurSpentOutputs(unconfirmedTxs) - val unconfirmedReceived = unconfirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash)) + val unconfirmedReceived = unconfirmedTxs.flatMap(_.txOut).filter(txOut => isReceive(txOut, scriptHash)) val confirmedBalance = confirmedReceived.map(_.amount).sum - confirmedSpents.map(_.amount).sum val unconfirmedBalance = unconfirmedReceived.map(_.amount).sum - unconfirmedSpents.map(_.amount).sum @@ -872,29 +873,29 @@ object ElectrumWallet { } /** - * - * @return the (confirmed, unconfirmed) balance for this wallet. This balance may not - * be up-to-date if we have not received all data we've asked for yet. - */ + * + * @return the (confirmed, unconfirmed) balance for this wallet. This balance may not + * be up-to-date if we have not received all data we've asked for yet. + */ lazy val balance: (Satoshi, Satoshi) = { // `.toList` is very important here: keys are returned in a Set-like structure, without the .toList we map // to another set-like structure that will remove duplicates, so if we have several script hashes with exactly the // same balance we don't return the correct aggregated balance val balances = (accountKeyMap.keys ++ changeKeyMap.keys).toList.map(scriptHash => balance(scriptHash)) - balances.foldLeft((Satoshi(0), Satoshi(0))) { + balances.foldLeft((0 sat, 0 sat)) { case ((confirmed, unconfirmed), (confirmed1, unconfirmed1)) => (confirmed + confirmed1, unconfirmed + unconfirmed1) } } /** - * Computes the effect of this transaction on the wallet - * - * @param tx input transaction - * @return an option: - * - Some(received, sent, fee) where sent if what the tx spends from us, received is what the tx sends to us, - * and fee is the fee for the tx) tuple where sent if what the tx spends from us, and received is what the tx sends to us - * - None if we are missing one or more parent txs - */ + * Computes the effect of this transaction on the wallet + * + * @param tx input transaction + * @return an option: + * - Some(received, sent, fee) where sent if what the tx spends from us, received is what the tx sends to us, + * and fee is the fee for the tx) tuple where sent if what the tx spends from us, and received is what the tx sends to us + * - None if we are missing one or more parent txs + */ def computeTransactionDelta(tx: Transaction): Option[(Satoshi, Satoshi, Option[Satoshi])] = { val ourInputs = tx.txIn.filter(isMine) // we need to make sure that for all inputs spending an output we control, we already have the parent tx @@ -912,12 +913,12 @@ object ElectrumWallet { } /** - * - * @param tx input transaction - * @param utxos input uxtos - * @return a tx where all utxos have been added as inputs, signed with dummy invalid signatures. This - * is used to estimate the weight of the signed transaction - */ + * + * @param tx input transaction + * @param utxos input uxtos + * @return a tx where all utxos have been added as inputs, signed with dummy invalid signatures. This + * is used to estimate the weight of the signed transaction + */ def addUtxosWithDummySig(tx: Transaction, utxos: Seq[Utxo]): Transaction = tx.copy(txIn = utxos.map { case utxo => // we use dummy signature here, because the result is only used to estimate fees @@ -928,77 +929,78 @@ object ElectrumWallet { }) /** - * - * @param amount amount we want to pay - * @param allowSpendUnconfirmed if true, use unconfirmed utxos - * @return a set of utxos with a total value that is greater than amount - */ - def chooseUtxos(amount: Satoshi, allowSpendUnconfirmed: Boolean): Seq[Utxo] = { - @tailrec - def select(chooseFrom: Seq[Utxo], selected: Set[Utxo]): Set[Utxo] = { - if (totalAmount(selected) >= amount) selected - else if (chooseFrom.isEmpty) throw new IllegalArgumentException("insufficient funds") - else select(chooseFrom.tail, selected + chooseFrom.head) - } - - // select utxos that are not locked by pending txs - val lockedOutputs = locks.map(_.txIn.map(_.outPoint)).flatten - val unlocked = utxos.filterNot(utxo => lockedOutputs.contains(utxo.outPoint)) - val unlocked1 = if (allowSpendUnconfirmed) unlocked else unlocked.filter(_.item.height > 0) - - // sort utxos by amount, in increasing order - // this way we minimize the number of utxos in the wallet, and so we minimize the fees we'll pay for them - val unlocked2 = unlocked1.sortBy(_.item.value) - val selected = select(unlocked2, Set()) - selected.toSeq - } - - /** - * - * @param tx input tx that has no inputs - * @param feeRatePerKw fee rate per kiloweight - * @param minimumFee minimum fee - * @param dustLimit dust limit - * @return a (state, tx, fee) tuple where state has been updated and tx is a complete, - * fully signed transaction that can be broadcast. - * our utxos spent by this tx are locked and won't be available for spending - * until the tx has been cancelled. If the tx is committed, they will be removed - */ + * + * @param tx input tx that has no inputs + * @param feeRatePerKw fee rate per kiloweight + * @param minimumFee minimum fee + * @param dustLimit dust limit + * @return a (state, tx, fee) tuple where state has been updated and tx is a complete, + * fully signed transaction that can be broadcast. + * our utxos spent by this tx are locked and won't be available for spending + * until the tx has been cancelled. If the tx is committed, they will be removed + */ def completeTransaction(tx: Transaction, feeRatePerKw: Long, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction, Satoshi) = { require(tx.txIn.isEmpty, "cannot complete a tx that already has inputs") require(feeRatePerKw >= 0, "fee rate cannot be negative") val amount = tx.txOut.map(_.amount).sum require(amount > dustLimit, "amount to send is below dust limit") - // start with a hefty fee estimate - val utxos = chooseUtxos(amount + Transactions.weight2fee(feeRatePerKw, 1000), allowSpendUnconfirmed) - val spent = totalAmount(utxos) - - // add utxos, and sign with dummy sigs - val tx1 = addUtxosWithDummySig(tx, utxos) + val unlocked = { + // select utxos that are not locked by pending txs + val lockedOutputs = locks.flatMap(_.txIn.map(_.outPoint)) + val unlocked1 = utxos.filterNot(utxo => lockedOutputs.contains(utxo.outPoint)) + val unlocked2 = if (allowSpendUnconfirmed) unlocked1 else unlocked1.filter(_.item.height > 0) + // sort utxos by amount, in increasing order + // this way we minimize the number of utxos in the wallet, and so we minimize the fees we'll pay for them + unlocked2.sortBy(_.item.value) + } - // compute the actual fee that we should pay - val fee1 = { - // add a dummy change output, which will be needed most of the time - val tx2 = tx1.addOutput(TxOut(amount, computePublicKeyScript(currentChangeKey.publicKey))) + // computes the fee what we would have to pay for our tx with our candidate utxos and an optional change output + def computeFee(candidates: Seq[Utxo], change: Option[TxOut]): Satoshi = { + val tx1 = addUtxosWithDummySig(tx, candidates) + val tx2 = change.map(o => tx1.addOutput(o)).getOrElse(tx1) Transactions.weight2fee(feeRatePerKw, tx2.weight()) } - // add change output only if non-dust, otherwise change is added to the fee - val (tx2, fee2, pos) = (spent - amount - fee1) match { - case dustChange if dustChange < dustLimit => (tx1, fee1 + dustChange, -1) // if change is below dust we add it to fees - case change => (tx1.addOutput(TxOut(change, computePublicKeyScript(currentChangeKey.publicKey))), fee1, 1) // change output index is always 1 + val dummyChange = TxOut(Satoshi(0), computePublicKeyScript(currentChangeKey.publicKey)) + + @tailrec + def loop(current: Seq[Utxo], remaining: Seq[Utxo]): (Seq[Utxo], Option[TxOut]) = { + totalAmount(current) match { + case total if total - computeFee(current, None) < amount && remaining.isEmpty => + // not enough funds to send amount and pay fees even without a change output + throw new IllegalArgumentException("insufficient funds") + case total if total - computeFee(current, None) < amount => + // not enough funds, try with an additional input + loop(remaining.head +: current, remaining.tail) + case total if total - computeFee(current, None) <= amount + dustLimit => + // change output would be below dust, we don't add one and just overpay fees + (current, None) + case total if total - computeFee(current, Some(dummyChange)) <= amount + dustLimit && remaining.isEmpty => + // change output is above dust limit but cannot pay for it's own fee, and we have no more utxos => we overpay a bit + (current, None) + case total if total - computeFee(current, Some(dummyChange)) <= amount + dustLimit => + // try with an additional input + loop(remaining.head +: current, remaining.tail) + case total => + val fee = computeFee(current, Some(dummyChange)) + val change = dummyChange.copy(amount = total - amount - fee) + (current, Some(change)) + } } + val (selected, change_opt) = loop(Seq.empty[Utxo], unlocked) + // sign our tx + val tx1 = addUtxosWithDummySig(tx, selected) + val tx2 = change_opt.map(out => tx1.addOutput(out)).getOrElse(tx1) val tx3 = signTransaction(tx2) - //Transaction.correctlySpends(tx3, utxos.map(utxo => utxo.outPoint -> TxOut(Satoshi(utxo.item.value), computePublicKeyScript(utxo.key.publicKey))).toMap, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - // and add the completed tx to the lokcs + // and add the completed tx to the locks val data1 = this.copy(locks = this.locks + tx3) - val fee3 = spent - tx3.txOut.map(_.amount).sum + val fee = selected.map(s => Satoshi(s.item.value)).sum - tx3.txOut.map(_.amount).sum - (data1, tx3, fee3) + (data1, tx3, fee) } def signTransaction(tx: Transaction): Transaction = { @@ -1013,19 +1015,19 @@ object ElectrumWallet { } /** - * unlocks input locked by a pending tx. call this method if the tx will not be used after all - * - * @param tx pending transaction - * @return an updated state - */ + * unlocks input locked by a pending tx. call this method if the tx will not be used after all + * + * @param tx pending transaction + * @return an updated state + */ def cancelTransaction(tx: Transaction): Data = this.copy(locks = this.locks - tx) /** - * remove all our utxos spent by this tx. call this method if the tx was broadcast successfully - * - * @param tx pending transaction - * @return an updated state - */ + * remove all our utxos spent by this tx. call this method if the tx was broadcast successfully + * + * @param tx pending transaction + * @return an updated state + */ def commitTransaction(tx: Transaction): Data = { // HACK! since we base our utxos computation on the history as seen by the electrum server (so that it is // reorg-proof out of the box), we need to update the history right away if we want to be able to build chained @@ -1045,14 +1047,14 @@ object ElectrumWallet { } /** - * spend all our balance, including unconfirmed utxos and locked utxos (i.e utxos - * that are used in funding transactions that have not been published yet - * - * @param publicKeyScript script to send all our funds to - * @param feeRatePerKw fee rate in satoshi per kiloweight - * @return a (tx, fee) tuple, tx is a signed transaction that spends all our balance and - * fee is the associated bitcoin network fee - */ + * spend all our balance, including unconfirmed utxos and locked utxos (i.e utxos + * that are used in funding transactions that have not been published yet + * + * @param publicKeyScript script to send all our funds to + * @param feeRatePerKw fee rate in satoshi per kiloweight + * @return a (tx, fee) tuple, tx is a signed transaction that spends all our balance and + * fee is the associated bitcoin network fee + */ def spendAll(publicKeyScript: ByteVector, feeRatePerKw: Long): (Transaction, Satoshi) = { // use confirmed and unconfirmed balance val amount = balance._1 + balance._2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala index 4f49a1f3cc..48aab4bbe3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala @@ -16,21 +16,21 @@ package fr.acinq.eclair.blockchain.electrum -import java.net.InetSocketAddress +import java.util.concurrent.atomic.AtomicLong -import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props, Stash, Terminated} -import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} +import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated} +import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{SSL, computeScriptHash} -import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT, BITCOIN_PARENT_TX_CONFIRMED} +import fr.acinq.eclair.blockchain.electrum.ElectrumClient.computeScriptHash +import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.{Globals, ShortChannelId, TxCoordinates} +import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, TxCoordinates} import scala.collection.SortedMap import scala.collection.immutable.Queue -class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLogging { +class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor with Stash with ActorLogging { client ! ElectrumClient.AddStatusListener(self) @@ -42,7 +42,7 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi val fakeFundingTx = Transaction( version = 2, txIn = Seq.empty[TxIn], - txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format + txOut = List.fill(outputIndex + 1)(TxOut(0 sat, pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format lockTime = 0) sender ! ValidateResult(c, Right((fakeFundingTx, UtxoStatus.Unspent))) @@ -163,7 +163,7 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi case ElectrumClient.ServerError(ElectrumClient.GetTransaction(txid, Some(origin: ActorRef)), _) => origin ! GetTxWithMetaResponse(txid, None, tip.time) case PublishAsap(tx) => - val blockCount = Globals.blockCount.get() + val blockCount = this.blockCount.get() val cltvTimeout = Scripts.cltvTimeout(tx) val csvTimeout = Scripts.csvTimeout(tx) if (csvTimeout > 0) { @@ -184,7 +184,7 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _, _) => log.info(s"parent tx of txid=${tx.txid} has been confirmed") - val blockCount = Globals.blockCount.get() + val blockCount = this.blockCount.get() val csvTimeout = Scripts.csvTimeout(tx) val absTimeout = blockHeight + csvTimeout if (absTimeout > blockCount) { @@ -212,38 +212,3 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi } } - -object ElectrumWatcher extends App { - - val system = ActorSystem() - - import scala.concurrent.ExecutionContext.Implicits.global - - class Root extends Actor with ActorLogging { - val client = context.actorOf(Props(new ElectrumClient(new InetSocketAddress("localhost", 51000), ssl = SSL.OFF)), "client") - client ! ElectrumClient.AddStatusListener(self) - - override def unhandled(message: Any): Unit = { - super.unhandled(message) - log.warning(s"unhandled message $message") - } - - def receive = { - case ElectrumClient.ElectrumReady(_, _, _) => - log.info(s"starting watcher") - context become running(context.actorOf(Props(new ElectrumWatcher(client)), "watcher")) - } - - def running(watcher: ActorRef): Receive = { - case watch: Watch => watcher forward watch - } - } - - val root = system.actorOf(Props[Root], "root") - val scanner = new java.util.Scanner(System.in) - while (true) { - val tx = Transaction.read(scanner.nextLine()) - root ! WatchSpent(root, tx.txid, 0, tx.txOut(0).publicKeyScript, BITCOIN_FUNDING_SPENT) - root ! WatchConfirmed(root, tx.txid, tx.txOut(0).publicKeyScript, 4L, BITCOIN_FUNDING_DEPTHOK) - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala index f817dc3e21..e0611d517d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala @@ -46,7 +46,7 @@ class SqliteWalletDb(sqlite: Connection) extends WalletDb { } override def addHeaders(startHeight: Int, headers: Seq[BlockHeader]): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)"), disableAutoCommit = true) { statement => + using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)"), inTransaction = true) { statement => var height = startHeight headers.foreach(header => { statement.setInt(1, height) 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 b25bf1d900..759cab34dd 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 @@ -24,18 +24,18 @@ import org.json4s.JsonAST._ import scala.concurrent.{ExecutionContext, Future} /** - * Created by PM on 09/07/2017. - */ + * Created by PM on 09/07/2017. + */ class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerKB)(implicit ec: ExecutionContext) extends FeeProvider { implicit val formats = DefaultFormats.withBigDecimal /** - * We need this to keep commitment tx fees in sync with the state of the network - * - * @param nBlocks number of blocks until tx is confirmed - * @return the current fee estimate in Satoshi/KB - */ + * We need this to keep commitment tx fees in sync with the state of the network + * + * @param nBlocks number of blocks until tx is confirmed + * @return the current fee estimate in Satoshi/KB + */ def estimateSmartFee(nBlocks: Int): Future[Long] = rpcClient.invoke("estimatesmartfee", nBlocks).map(BitcoinCoreFeeProvider.parseFeeEstimate) @@ -64,16 +64,16 @@ object BitcoinCoreFeeProvider { json \ "feerate" match { case JDecimal(feerate) => // estimatesmartfee returns a fee rate in Btc/KB - btc2satoshi(Btc(feerate)).amount + btc2satoshi(Btc(feerate)).toLong case JInt(feerate) if feerate.toLong < 0 => // negative value means failure feerate.toLong case JInt(feerate) => // should (hopefully) never happen - btc2satoshi(Btc(feerate.toLong)).amount + btc2satoshi(Btc(feerate.toLong)).toLong } case JArray(errors) => - val error = errors collect { case JString(error) => error } mkString (", ") + val error = errors.collect { case JString(error) => error }.mkString(", ") throw new RuntimeException(s"estimatesmartfee failed: $error") case _ => throw new RuntimeException("estimatesmartfee failed") 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 df18977172..bdd0a24e13 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 @@ -26,4 +26,4 @@ trait FeeEstimator { case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) -case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, maxFeerateMismatch: Double, updateFeeMinDiffRatio: Double) \ No newline at end of file +case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, maxFeerateMismatch: Double, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double) \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 939dafc43b..4ee22c5faa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, FSM, OneForOneStrategy, Props, Status, SupervisorStrategy} import akka.event.Logging.MDC import akka.pattern.pipe -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey, sha256} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Helpers.{Closing, Funding} @@ -48,21 +48,21 @@ object Channel { val ANNOUNCEMENTS_MINCONF = 6 // https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements - val MAX_FUNDING_SATOSHIS = 16777216L // = 2^24 + val MAX_FUNDING = 16777216 sat // = 2^24 val MAX_ACCEPTED_HTLCS = 483 // we don't want the counterparty to use a dust limit lower than that, because they wouldn't only hurt themselves we may need them to publish their commit tx in certain cases (backup/restore) - val MIN_DUSTLIMIT = 546 + val MIN_DUSTLIMIT = 546 sat // 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 = 9L - val MAX_CLTV_EXPIRY = 7 * 144L // one week + val MIN_CLTV_EXPIRY_DELTA = CltvExpiryDelta(9) + val MAX_CLTV_EXPIRY_DELTA = 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 = 2016 + val MAX_TO_SELF_DELAY = CltvExpiryDelta(2016) // as a fundee, we will wait that much time for the funding tx to confirm (funder will rely on the funding tx being double-spent) val FUNDING_TIMEOUT_FUNDEE = 5 days @@ -145,26 +145,28 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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, _, channelFlags), Nothing) => + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, channelVersion), Nothing) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) forwarder ! remote + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, fundingSatoshis = fundingSatoshis, pushMsat = pushMsat, - dustLimitSatoshis = localParams.dustLimitSatoshis, + dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserveSatoshis, - htlcMinimumMsat = localParams.htlcMinimumMsat, + channelReserveSatoshis = localParams.channelReserve, + htlcMinimumMsat = localParams.htlcMinimum, feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0), + fundingPubkey = fundingPubKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), channelFlags = channelFlags) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open @@ -227,7 +229,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val candidateChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, - normal.commitments.remoteParams.htlcMinimumMsat, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) + normal.commitments.remoteParams.htlcMinimum, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { // if there was no configuration change we keep the existing channel update normal.channelUpdate @@ -271,28 +273,31 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Failure(t) => handleLocalError(t, d, Some(open)) case Success(_) => context.system.eventStream.publish(ChannelCreated(self, context.parent, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelVersion = ChannelVersion.STANDARD + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = nodeParams.minDepthBlocks val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, - dustLimitSatoshis = localParams.dustLimitSatoshis, + dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserveSatoshis, + channelReserveSatoshis = localParams.channelReserve, minimumDepth = minimumDepth, - htlcMinimumMsat = localParams.htlcMinimumMsat, + htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, - revocationBasepoint = keyManager.revocationPoint(localParams.channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(localParams.channelKeyPath).publicKey, - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(localParams.channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0)) + fundingPubkey = fundingPubkey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0)) val remoteParams = RemoteParams( nodeId = remoteNodeId, - dustLimitSatoshis = open.dustLimitSatoshis, + dustLimit = open.dustLimitSatoshis, maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserveSatoshis = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimumMsat = open.htlcMinimumMsat, + 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, @@ -303,7 +308,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"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, accept) sending accept + 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, channelVersion, accept) sending accept } case Event(CMD_CLOSE(_), _) => goto(CLOSED) replying "ok" @@ -314,7 +319,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) 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, _), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelVersion), open)) => log.info(s"received AcceptChannel=$accept") Try(Helpers.validateParamsFunder(nodeParams, open, accept)) match { case Failure(t) => handleLocalError(t, d, Some(accept)) @@ -322,10 +327,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // TODO: check equality of temporaryChannelId? or should be done upstream val remoteParams = RemoteParams( nodeId = remoteNodeId, - dustLimitSatoshis = accept.dustLimitSatoshis, + dustLimit = accept.dustLimitSatoshis, maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserveSatoshis = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimumMsat = accept.htlcMinimumMsat, + 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, @@ -336,10 +341,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId globalFeatures = remoteInit.globalFeatures, localFeatures = remoteInit.localFeatures) log.debug(s"remote params: $remoteParams") - val localFundingPubkey = keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open) + 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, channelVersion, open) } case Event(CMD_CLOSE(_), _) => @@ -360,11 +365,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) => + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelVersion, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath)) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -376,7 +381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.parent ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages 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, fundingCreated) sending fundingCreated + 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, channelVersion, fundingCreated) sending fundingCreated case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => log.error(t, s"wallet returned error: ") @@ -401,17 +406,18 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, _)) => + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelVersion, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, temporaryChannelId, localParams, remoteParams, fundingSatoshis: Long, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) + val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(keyManager, channelVersion, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.onChainFeeConf.maxFeerateMismatch) // check remote signature validity - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey) val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) // watch the funding tx transaction val commitInput = localCommitTx.input @@ -419,7 +425,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId channelId = channelId, signature = localSigOfRemoteTx ) - val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags, + val commitments = Commitments(channelVersion, localParams, remoteParams, channelFlags, LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 0L, remoteNextHtlcId = 0L, @@ -445,10 +451,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) 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, fundingCreated)) => + case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelVersion, fundingCreated)) => // we make sure that their sig checks out and that our first commit tx is spendable - val localSigOfLocalTx = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey) + 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 @@ -457,7 +464,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) case Success(_) => val commitInput = localCommitTx.input - val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags, + val commitments = Commitments(channelVersion, localParams, remoteParams, channelFlags, LocalCommit(0, localSpec, PublishableTxs(signedLocalCommitTx, Nil)), remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 0L, remoteNextHtlcId = 0L, @@ -467,24 +474,26 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val now = Platform.currentTime.milliseconds.toSeconds context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - // we do this to make sure that the channel state has been written to disk when we publish the funding tx - val nextState = store(DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), now, None, Left(fundingCreated))) blockchain ! WatchSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) log.info(s"committing txid=${fundingTx.txid}") - wallet.commit(fundingTx).onComplete { - case Success(true) => - // NB: funding tx isn't confirmed at this point, so technically we didn't really pay the network fee yet, so this is a (fair) approximation - feePaid(fundingTxFee, fundingTx, "funding", commitments.channelId) - replyToUser(Right(s"created channel $channelId")) - case Success(false) => - replyToUser(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) => - replyToUser(Left(LocalError(t))) - log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast + // 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) => + // NB: funding tx isn't confirmed at this point, so technically we didn't really pay the network fee yet, so this is a (fair) approximation + feePaid(fundingTxFee, fundingTx, "funding", commitments.channelId) + replyToUser(Right(s"created channel $channelId")) + case Success(false) => + replyToUser(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) => + replyToUser(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 nextState + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), now, None, Left(fundingCreated)) storing() calling(publishFundingTx) } case Event(CMD_CLOSE(_) | CMD_FORCECLOSE, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -522,7 +531,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) - val nextPerCommitmentPoint = keyManager.commitmentPoint(commitments.localParams.channelKeyPath, 1) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, commitments.channelVersion) + 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 @@ -561,7 +571,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId)) // 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 initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimum, nodeParams.feeBase, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, 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.schedule(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, interval = 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) storing() @@ -599,7 +609,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), Some(d.channelUpdate), Some(c)), c) case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => - Try(Commitments.sendAdd(d.commitments, c, origin(c))) match { + Try(Commitments.sendAdd(d.commitments, c, origin(c), nodeParams.currentBlockHeight)) match { case Success(Right((commitments1, add))) => if (c.commit) self ! CMD_SIGN handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending add @@ -704,7 +714,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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 - val trimmedHtlcs = Transactions.trimOfferedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(Satoshi(d.commitments.remoteParams.dustLimitSatoshis), nextRemoteCommit.spec) + val trimmedHtlcs = Transactions.trimOfferedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) ++ Transactions.trimReceivedHtlcs(d.commitments.remoteParams.dustLimit, nextRemoteCommit.spec) trimmedHtlcs collect { case DirectedHtlc(_, u) => log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber") @@ -716,9 +726,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId self ! BroadcastChannelUpdate(AboveReserve) } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) - if (nextRemoteCommit.spec.toRemoteMsat != d.commitments.remoteCommit.spec.toRemoteMsat) { + if (nextRemoteCommit.spec.toRemote != d.commitments.remoteCommit.spec.toRemote) { // we send this event only when our balance changes (note that remoteCommit.toRemote == toLocal) - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemoteMsat, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemote, commitments1)) } // we expect a quick response from our peer setTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer = context.parent), timeout = nodeParams.revocationTimeout, repeat = false) @@ -852,15 +862,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentBlockCount, d: DATA_NORMAL) => handleNewBlock(c, d) case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) => - val networkFeeratePerKw = feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) - d.commitments.localParams.isFunder match { - case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => - self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) - stay - case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => - handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) - case _ => stay - } + handleCurrentFeerate(c, d) case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex, _), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => val shortChannelId = ShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt) @@ -891,7 +893,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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 channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + 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) // we use GOTO instead of stay because we want to fire transitions goto(NORMAL) using manualTransition(NORMAL, NORMAL, d, d.copy(channelAnnouncement = Some(channelAnn))) storing() case Some(_) => @@ -1133,16 +1136,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentBlockCount, d: DATA_SHUTDOWN) => handleNewBlock(c, d) - case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_SHUTDOWN) => - val networkFeeratePerKw = feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) - d.commitments.localParams.isFunder match { - case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => - self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) - stay - case false if Helpers.isFeeDiffTooHigh(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => - handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) - case _ => stay - } + case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) => + handleCurrentFeerate(c, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) @@ -1159,21 +1154,21 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(NEGOTIATING)(handleExceptions { case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig), d: DATA_NEGOTIATING) => log.info(s"received closingFeeSatoshis=$remoteClosingFee") - Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(remoteClosingFee), remoteSig) match { + Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { case Success(signedClosingTx) if d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).contains(remoteClosingFee) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => // we close when we converge or when there were too many iterations handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) case Success(signedClosingTx) => // 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 lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis).map(Satoshi) + val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) val nextClosingFee = Closing.nextClosingFee( localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)), - remoteClosingFee = Satoshi(remoteClosingFee)) + remoteClosingFee = remoteClosingFee) val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) if (lastLocalClosingFee.contains(nextClosingFee)) { // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) - } else if (nextClosingFee == Satoshi(remoteClosingFee)) { + } else if (nextClosingFee == remoteClosingFee) { // we have converged! val closingTxProposed1 = d.closingTxProposed match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx.tx, closingSigned)) @@ -1210,25 +1205,17 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Try(Commitments.sendFulfill(d.commitments, c)) match { case Success((commitments1, _)) => log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") - val localCommitPublished1 = d.localCommitPublished.map { - localCommitPublished => - val localCommitPublished1 = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(localCommitPublished1) - localCommitPublished1 - } - val remoteCommitPublished1 = d.remoteCommitPublished.map { - remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(remoteCommitPublished1) - remoteCommitPublished1 + val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) + val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) + val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) + + def republish(): Unit = { + localCommitPublished1.foreach(doPublish) + remoteCommitPublished1.foreach(doPublish) + nextRemoteCommitPublished1.foreach(doPublish) } - val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map { - remoteCommitPublished => - val remoteCommitPublished1 = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(remoteCommitPublished1) - remoteCommitPublished1 - } - stay using d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1) storing() + + stay using d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1) storing() calling(republish) case Failure(cause) => handleCommandError(cause, c) } @@ -1306,13 +1293,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val revokedCommitPublished1 = d.revokedCommitPublished.map(Closing.updateRevokedCommitPublished(_, tx)) // if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund if (localCommitPublished1.map(_.commitTx.txid).contains(tx.txid)) { - context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.remoteParams.toSelfDelay)) + context.system.eventStream.publish(LocalCommitConfirmed(self, remoteNodeId, d.channelId, blockHeight + d.commitments.remoteParams.toSelfDelay.toInt)) } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedoutHtlcs = - Closing.timedoutHtlcs(d.commitments.localCommit, Satoshi(d.commitments.localParams.dustLimitSatoshis), tx) ++ - Closing.timedoutHtlcs(d.commitments.remoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx) ++ - d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, Satoshi(d.commitments.remoteParams.dustLimitSatoshis), tx)) + Closing.timedoutHtlcs(d.commitments.localCommit, d.commitments.localParams.dustLimit, tx) ++ + Closing.timedoutHtlcs(d.commitments.remoteCommit, d.commitments.remoteParams.dustLimit, tx) ++ + d.commitments.remoteNextCommitInfo.left.toSeq.flatMap(r => Closing.timedoutHtlcs(r.nextRemoteCommit, d.commitments.remoteParams.dustLimit, tx)) timedoutHtlcs.foreach { add => d.commitments.originChannels.get(add.id) match { case Some(origin) => @@ -1339,7 +1326,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Closing .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) .map(add => (add, d.commitments.originChannels.get(add.id).collect { case Local(id, _) => id })) // we resolve the payment id if this was a local payment - .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = MilliSatoshi(add.amountMsat), add.paymentHash)) } + .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = add.amountMsat, add.paymentHash)) } // we update the channel data val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) // and we also send events related to fee @@ -1405,7 +1392,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId forwarder ! r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) - val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, d.commitments.localCommit.index) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) + val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index) val channelReestablish = ChannelReestablish( channelId = d.channelId, @@ -1425,6 +1413,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // -> 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(c: CurrentBlockCount, d: HasCommitments) => handleNewBlock(c, d) + case Event(c: CurrentFeerates, d: HasCommitments) => + handleOfflineFeerate(c, d) + case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => @@ -1462,16 +1453,18 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) channelReestablish match { case ChannelReestablish(_, _, nextRemoteRevocationNumber, Some(yourLastPerCommitmentSecret), _) if !Helpers.checkLocalCommit(d, nextRemoteRevocationNumber) => // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1 - if (keyManager.commitmentSecret(d.commitments.localParams.channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { + if (keyManager.commitmentSecret(channelKeyPath, nextRemoteRevocationNumber - 1) == yourLastPerCommitmentSecret) { log.warning(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${d.commitments.localCommit.index} theirCommitmentNumber=$nextRemoteRevocationNumber") // their data checks out, we indeed seem to be using an old revoked commitment, and must absolutely *NOT* publish it, because that would be a cheating attempt and they // would punish us by taking all the funds in the channel @@ -1496,7 +1489,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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(s"re-sending fundingLocked") - val nextPerCommitmentPoint = keyManager.commitmentPoint(d.commitments.localParams.channelKeyPath, 1) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) forwarder ! fundingLocked } @@ -1559,6 +1552,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentBlockCount, d: HasCommitments) => handleNewBlock(c, d) + case Event(c: CurrentFeerates, d: HasCommitments) => + handleOfflineFeerate(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) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) @@ -1736,6 +1732,42 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId origin_opt.foreach(_ ! m) } + def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { + val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) + val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw + d.commitments.localParams.isFunder match { + case true if Helpers.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => + self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) + stay + case false if Helpers.isFeeDiffTooHigh(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => + handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) + case _ => stay + } + } + + /** + * This is used to check for the commitment fees when the channel is not operational but we have something at stake + * @param c the new feerates + * @param d the channel commtiments + * @return + */ + def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = { + val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) + val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw + // if the fees are too high we risk to not be able to confirm our current commitment + if(networkFeeratePerKw > currentFeeratePerKw && Helpers.isFeeDiffTooHigh(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)){ + if(nodeParams.onChainFeeConf.closeOnOfflineMismatch) { + log.warning(s"closing OFFLINE ${d.channelId} due to fee mismatch: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") + handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = currentFeeratePerKw, remoteFeeratePerKw = networkFeeratePerKw), d, Some(c)) + } else { + log.warning(s"channel ${d.channelId} is OFFLINE but its fee mismatch is over the threshold: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") + stay + } + } else { + stay + } + } + def handleCommandSuccess(sender: ActorRef, newData: Data) = { stay using newData replying "ok" } @@ -1746,7 +1778,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _: ChannelException => () case _ => log.error(cause, s"msg=$cmd stateData=$stateData ") } - context.system.eventStream.publish(ChannelErrorOccured(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(cause), isFatal = false)) + context.system.eventStream.publish(ChannelErrorOccurred(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(cause), isFatal = false)) stay replying Status.Failure(cause) } @@ -1799,7 +1831,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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(ChannelErrorOccured(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(exc), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(exc), isFatal = true)) goto(CLOSED) sending error } @@ -1807,7 +1839,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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(ChannelErrorOccured(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(exc), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(exc), isFatal = true)) goto(CLOSED) sending error } @@ -1877,7 +1909,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _ => log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData ") } val error = Error(Helpers.getChannelId(d), cause.getMessage) - context.system.eventStream.publish(ChannelErrorOccured(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(cause), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, LocalError(cause), isFatal = true)) d match { case dd: HasCommitments if Closing.nothingAtStake(dd) => goto(CLOSED) @@ -1893,7 +1925,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleRemoteError(e: Error, d: Data) = { // 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(ChannelErrorOccured(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, RemoteError(e), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, Helpers.getChannelId(stateData), remoteNodeId, stateData, RemoteError(e), isFatal = true)) d match { case _: DATA_CLOSING => stay // nothing to do, there is already a spending tx published @@ -1908,13 +1940,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleMutualClose(closingTx: Transaction, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { log.info(s"closing tx published: closingTxId=${closingTx.txid}") - doPublish(closingTx) - val nextData = d match { case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) } - goto(CLOSING) using nextData storing() + + goto(CLOSING) using nextData storing() calling(doPublish(closingTx)) } def doPublish(closingTx: Transaction): Unit = { @@ -1937,7 +1968,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(localCommitPublished) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) @@ -1946,7 +1976,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) } - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() calling(doPublish(localCommitPublished)) } } @@ -2002,7 +2032,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(remoteCommitPublished) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) @@ -2011,7 +2040,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() calling(doPublish(remoteCommitPublished)) } def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { @@ -2021,8 +2050,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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 = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - doPublish(remoteCommitPublished) - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() calling(doPublish(remoteCommitPublished)) } def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { @@ -2032,7 +2060,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(commitTx.txid == remoteCommit.txid, "txid mismatch") val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(remoteCommitPublished) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -2041,7 +2068,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() calling(doPublish(remoteCommitPublished)) } def doPublish(remoteCommitPublished: RemoteCommitPublished): Unit = { @@ -2070,15 +2097,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val exc = FundingTxSpent(d.channelId, tx) val error = Error(d.channelId, exc.getMessage) - doPublish(revokedCommitPublished) - 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 = now, 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 = now, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) } - goto(CLOSING) using nextData storing() sending error + 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!!!") @@ -2112,9 +2137,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's try to spend our current local tx val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - doPublish(localCommitPublished) - goto(ERR_INFORMATION_LEAK) sending error + goto(ERR_INFORMATION_LEAK) calling(doPublish(localCommitPublished)) sending error } def handleSync(channelReestablish: ChannelReestablish, d: HasCommitments): Commitments = { @@ -2136,8 +2160,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } else if (commitments1.localCommit.index == channelReestablish.nextRemoteRevocationNumber + 1) { // our last revocation got lost, let's resend it log.debug(s"re-sending last revocation") - val localPerCommitmentSecret = keyManager.commitmentSecret(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index - 1) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(commitments1.localParams.channelKeyPath, d.commitments.localCommit.index + 1) + val channelKeyPath = keyManager.channelKeyPath(d.commitments.localParams, d.commitments.channelVersion) + val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, d.commitments.localCommit.index - 1) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommit.index + 1) val revocation = RevokeAndAck( channelId = commitments1.channelId, perCommitmentSecret = localPerCommitmentSecret, @@ -2215,21 +2240,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def origin(c: CMD_ADD_HTLC): Origin = c.upstream match { case Left(id) => Local(id, Some(sender)) // we were the origin of the payment - case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment + case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amount) // this is a relayed payment } def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32): Unit = { - log.info(s"paid feeSatoshi=${fee.amount} for txid=${tx.txid} desc=$desc") + log.info(s"paid feeSatoshi=${fee.toLong} for txid=${tx.txid} desc=$desc") context.system.eventStream.publish(NetworkFeePaid(self, remoteNodeId, channelId, tx, fee, desc)) } - def store(d: HasCommitments) = { - log.debug(s"updating database record for channelId={}", d.channelId) - nodeParams.db.channels.addOrUpdateChannel(d) - context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) - d - } - implicit def state2mystate(state: FSM.State[fr.acinq.eclair.channel.State, Data]): MyState = MyState(state) case class MyState(state: FSM.State[fr.acinq.eclair.channel.State, Data]) { @@ -2237,7 +2255,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def storing(): FSM.State[fr.acinq.eclair.channel.State, Data] = { state.stateData match { case d: HasCommitments => - store(d) + log.debug(s"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}") @@ -2255,6 +2275,15 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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[fr.acinq.eclair.channel.State, Data] = { + f + state + } + } def now = Platform.currentTime.milliseconds.toSeconds 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 e9104a7db4..bf6441b0d8 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 @@ -19,7 +19,7 @@ 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.ShortChannelId +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} import fr.acinq.eclair.channel.Channel.ChannelError import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} @@ -48,12 +48,12 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent -case class ChannelErrorOccured(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, data: Data, error: ChannelError, isFatal: Boolean) extends ChannelEvent +case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, data: Data, error: ChannelError, isFatal: Boolean) extends ChannelEvent case class NetworkFeePaid(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, tx: Transaction, fee: Satoshi, txType: String) extends ChannelEvent // NB: this event is only sent when the channel is available -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, localBalanceMsat: Long, commitments: Commitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, localBalance: MilliSatoshi, commitments: Commitments) extends ChannelEvent case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: Data) 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 3546f22af6..96aa735911 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 @@ -17,30 +17,30 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, Transaction} -import fr.acinq.eclair.UInt64 +import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.payment.Origin import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64} /** - * Created by PM on 11/04/2017. - */ + * Created by PM on 11/04/2017. + */ class ChannelException(val channelId: ByteVector32, message: String) extends RuntimeException(message) // @formatter:off case class DebugTriggeredException (override val channelId: ByteVector32) extends ChannelException(channelId, "debug-mode triggered failure") case class InvalidChainHash (override val channelId: ByteVector32, local: ByteVector32, remote: ByteVector32) extends ChannelException(channelId, s"invalid chainHash (local=$local remote=$remote)") -case class InvalidFundingAmount (override val channelId: ByteVector32, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)") -case class InvalidPushAmount (override val channelId: ByteVector32, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid pushMsat=$pushMsat (max=$max)") +case class InvalidFundingAmount (override val channelId: ByteVector32, fundingAmount: Satoshi, min: Satoshi, max: Satoshi) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingAmount (min=$min max=$max)") +case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)") case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)") -case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"dustLimitSatoshis=$dustLimitSatoshis is too small (min=$min)") -case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimitSatoshis: Long, max: Long) extends ChannelException(channelId, s"dustLimitSatoshis=$dustLimitSatoshis is too large (max=$max)") -case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimitSatoshis: Long, channelReserveSatoshis: Long) extends ChannelException(channelId, s"dustLimitSatoshis dustLimitSatoshis=$dustLimitSatoshis is above our channelReserveSatoshis=$channelReserveSatoshis") -case class ToSelfDelayTooHigh (override val channelId: ByteVector32, toSelfDelay: Int, max: Int) extends ChannelException(channelId, s"unreasonable to_self_delay=$toSelfDelay (max=$max)") -case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio") -case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserveSatoshis: Long, dustLimitSatoshis: Long) extends ChannelException(channelId, s"their channelReserveSatoshis=$channelReserveSatoshis is below our dustLimitSatoshis=$dustLimitSatoshis") -case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocalMsat: Long, toRemoteMsat: Long, reserveSatoshis: Long) extends ChannelException(channelId, s"channel reserve is not met toLocalMsat=$toLocalMsat toRemoteMsat=$toRemoteMsat reserveSat=$reserveSatoshis") +case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)") +case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)") +case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve") +case class ToSelfDelayTooHigh (override val channelId: ByteVector32, toSelfDelay: CltvExpiryDelta, max: CltvExpiryDelta) extends ChannelException(channelId, s"unreasonable to_self_delay=$toSelfDelay (max=$max)") +case class ChannelReserveTooHigh (override val channelId: ByteVector32, channelReserve: Satoshi, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserve too high: reserve=$channelReserve fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio") +case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit") +case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve") case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error") case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class ClosingAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "closing already in progress") @@ -57,21 +57,22 @@ case class FeerateTooDifferent (override val channelId: ByteVect case class InvalidCommitmentSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid commitment signature: tx=$tx") case class InvalidHtlcSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid htlc signature: tx=$tx") case class InvalidCloseSignature (override val channelId: ByteVector32, tx: Transaction) extends ChannelException(channelId, s"invalid close signature: tx=$tx") -case class InvalidCloseFee (override val channelId: ByteVector32, feeSatoshi: Long) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$feeSatoshi") +case class InvalidCloseFee (override val channelId: ByteVector32, fee: Satoshi) extends ChannelException(channelId, s"invalid close fee: fee_satoshis=$fee") case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual") case class ForcedLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, s"forced local commit") case class UnexpectedHtlcId (override val channelId: ByteVector32, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual") -case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount") -case class ExpiryTooBig (override val channelId: ByteVector32, maximum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockCount=$blockCount") -case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") -case class HtlcValueTooHighInFlight (override val channelId: ByteVector32, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual") +case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: CltvExpiry, actual: CltvExpiry, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount") +case class ExpiryTooBig (override val channelId: ByteVector32, maximum: CltvExpiry, actual: CltvExpiry, blockCount: Long) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockCount=$blockCount") +case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: MilliSatoshi, actual: MilliSatoshi) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") +case class HtlcValueTooHighInFlight (override val channelId: ByteVector32, maximum: UInt64, actual: MilliSatoshi) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual") case class TooManyAcceptedHtlcs (override val channelId: ByteVector32, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum") -case class InsufficientFunds (override val channelId: ByteVector32, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis") +case class InsufficientFunds (override val channelId: ByteVector32, amount: MilliSatoshi, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"insufficient funds: missing=$missing reserve=$reserve fees=$fees") +case class RemoteCannotAffordFeesForNewHtlc (override val channelId: ByteVector32, amount: MilliSatoshi, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"remote can't afford increased commit tx fees once new HTLC is added: missing=$missing reserve=$reserve fees=$fees") 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 CannotAffordFees (override val channelId: ByteVector32, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis") +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") case class UnexpectedRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "received unexpected RevokeAndAck message") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 48e6d6b57f..c23af9ea06 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -24,13 +24,13 @@ import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, T import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} /** - * Created by PM on 20/05/2016. - */ + * Created by PM on 20/05/2016. + */ // @formatter:off @@ -75,7 +75,7 @@ case object ERR_INFORMATION_LEAK extends State 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ -case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) +case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte, channelVersion: ChannelVersion) case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init) 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 @@ -106,14 +106,14 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amount: MilliSatoshi, paymentHash: ByteVector32, cltvExpiry: CltvExpiry, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command final case class CMD_UPDATE_FEE(feeratePerKw: Long, commit: Boolean = false) extends Command final case object CMD_SIGN extends Command final case class CMD_CLOSE(scriptPubKey: Option[ByteVector]) extends Command -final case class CMD_UPDATE_RELAY_FEE(feeBaseMsat: Long, feeProportionalMillionths: Long) extends Command +final case class CMD_UPDATE_RELAY_FEE(feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends Command final case object CMD_FORCECLOSE extends Command final case object CMD_GETSTATE extends Command final case object CMD_GETSTATEDATA extends Command @@ -148,9 +148,9 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, lastSent: OpenChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, lastSent: AcceptChannel) extends Data -final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data +final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelVersion: ChannelVersion, lastSent: OpenChannel) extends Data +final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, channelVersion: ChannelVersion, lastSent: AcceptChannel) extends Data +final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, channelVersion: ChannelVersion, lastSent: FundingCreated) extends Data final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], waitingSince: Long, // how long have we been waiting for the funding tx to confirm @@ -190,12 +190,12 @@ final case class DATA_CLOSING(commitments: Commitments, final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments final case class LocalParams(nodeId: PublicKey, - channelKeyPath: DeterministicWallet.KeyPath, - dustLimitSatoshis: Long, - maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, - toSelfDelay: Int, + fundingKeyPath: DeterministicWallet.KeyPath, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + channelReserve: Satoshi, + htlcMinimum: MilliSatoshi, + toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, @@ -203,11 +203,11 @@ final case class LocalParams(nodeId: PublicKey, localFeatures: ByteVector) final case class RemoteParams(nodeId: PublicKey, - dustLimitSatoshis: Long, - maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, - toSelfDelay: Int, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + channelReserve: Satoshi, + htlcMinimum: MilliSatoshi, + toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, fundingPubKey: PublicKey, revocationBasepoint: PublicKey, @@ -224,9 +224,23 @@ object ChannelFlags { case class ChannelVersion(bits: BitVector) { require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes") + + def |(other: ChannelVersion) = ChannelVersion(bits | other.bits) + def &(other: ChannelVersion) = ChannelVersion(bits & other.bits) + def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits) + def isSet(bit: Int) = bits.reverse.get(bit) } + object ChannelVersion { + import scodec.bits._ val LENGTH_BITS = 4 * 8 - val STANDARD = ChannelVersion(BitVector.fill(LENGTH_BITS)(false)) + val ZEROES = ChannelVersion(bin"00000000000000000000000000000000") + val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 + + def fromBit(bit: Int) = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse) + + val USE_PUBKEY_KEYPATH = fromBit(USE_PUBKEY_KEYPATH_BIT) + + val STANDARD = ZEROES | USE_PUBKEY_KEYPATH } // @formatter:on 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 7c1bace272..27db4747c3 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 @@ -18,14 +18,14 @@ 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} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, UInt64} +import fr.acinq.eclair.{MilliSatoshi, _} // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { @@ -41,13 +41,13 @@ case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, // @formatter:on /** - * about remoteNextCommitInfo: - * we either: - * - have built and signed their next commit tx with their next revocation hash which can now be discarded - * - have their next per-commitment point - * So, when we've signed and sent a commit message and are waiting for their revocation message, - * theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point - */ + * about remoteNextCommitInfo: + * we either: + * - have built and signed their next commit tx with their next revocation hash which can now be discarded + * - have their next per-commitment point + * So, when we've signed and sent a commit message and are waiting for their revocation message, + * theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point + */ case class Commitments(channelVersion: ChannelVersion, localParams: LocalParams, remoteParams: RemoteParams, channelFlags: Byte, @@ -62,19 +62,19 @@ case class Commitments(channelVersion: ChannelVersion, def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = - (localCommit.spec.htlcs.filter(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry) ++ - remoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry) ++ - remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry)).getOrElse(Set.empty[DirectedHtlc])).map(_.add) + (localCommit.spec.htlcs.filter(htlc => htlc.direction == OUT && blockheight >= htlc.add.cltvExpiry.toLong) ++ + remoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry.toLong) ++ + remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.filter(htlc => htlc.direction == IN && blockheight >= htlc.add.cltvExpiry.toLong)).getOrElse(Set.empty[DirectedHtlc])).map(_.add) /** - * HTLCs that are close to timing out upstream are potentially dangerous. If we received the pre-image for those - * HTLCs, we need to get a remote signed updated commitment that removes this HTLC. - * Otherwise when we get close to the upstream timeout, we risk an on-chain race condition between their HTLC timeout - * and our HTLC success in case of a force-close. - */ - def almostTimedOutIncomingHtlcs(blockheight: Long, fulfillSafety: Int): Set[UpdateAddHtlc] = { + * HTLCs that are close to timing out upstream are potentially dangerous. If we received the pre-image for those + * HTLCs, we need to get a remote signed updated commitment that removes this HTLC. + * Otherwise when we get close to the upstream timeout, we risk an on-chain race condition between their HTLC timeout + * and our HTLC success in case of a force-close. + */ + def almostTimedOutIncomingHtlcs(blockheight: Long, fulfillSafety: CltvExpiryDelta): Set[UpdateAddHtlc] = { localCommit.spec.htlcs.collect { - case htlc if htlc.direction == IN && blockheight >= htlc.add.cltvExpiry - fulfillSafety => htlc.add + case htlc if htlc.direction == IN && blockheight >= (htlc.add.cltvExpiry - fulfillSafety).toLong => htlc.add } } @@ -84,28 +84,58 @@ case class Commitments(channelVersion: ChannelVersion, val announceChannel: Boolean = (channelFlags & 0x01) != 0 - lazy val availableBalanceForSendMsat: Long = { - val reduced = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val feesMsat = if (localParams.isFunder) Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), reduced).amount * 1000 else 0 - math.max(reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat, 0) + lazy val availableBalanceForSend: MilliSatoshi = { + // 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 commitFees = commitTxFee(remoteParams.dustLimit, reduced).toMilliSatoshi + val htlcFees = htlcOutputFee(reduced.feeratePerKw) + if (balanceNoFees - commitFees < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced)) { + // htlc will be trimmed + (balanceNoFees - commitFees).max(0 msat) + } else { + // htlc will have an output in the commitment tx, so there will be additional fees. + (balanceNoFees - commitFees - htlcFees).max(0 msat) + } + } else { + // The fundee doesn't pay on-chain fees. + balanceNoFees + } } - lazy val availableBalanceForReceiveMsat: Long = { + lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val feesMsat = if (localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(localParams.dustLimitSatoshis), reduced).amount * 1000 - math.max(reduced.toRemoteMsat - localParams.channelReserveSatoshis * 1000 - feesMsat, 0) + 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. + balanceNoFees + } else { + // The funder always pays the on-chain fees, so we must subtract that from the amount we can receive. + val commitFees = commitTxFee(localParams.dustLimit, reduced).toMilliSatoshi + val htlcFees = htlcOutputFee(reduced.feeratePerKw) + if (balanceNoFees - commitFees < receivedHtlcTrimThreshold(localParams.dustLimit, reduced)) { + // htlc will be trimmed + (balanceNoFees - commitFees).max(0 msat) + } else { + // htlc will have an output in the commitment tx, so there will be additional fees. + (balanceNoFees - commitFees - htlcFees).max(0 msat) + } + } } } object Commitments { /** - * Add a change to our proposed change list. - * - * @param commitments current commitments. - * @param proposal proposed change to add. - * @return an updated commitment instance. - */ + * Add a change to our proposed change list. + * + * @param commitments current commitments. + * @param proposal proposed change to add. + * @return an updated commitment instance. + */ private def addLocalProposal(commitments: Commitments, proposal: UpdateMessage): Commitments = commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed :+ proposal)) @@ -113,31 +143,29 @@ object Commitments { commitments.copy(remoteChanges = commitments.remoteChanges.copy(proposed = commitments.remoteChanges.proposed :+ proposal)) /** - * - * @param commitments current commitments - * @param cmd add HTLC command - * @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc) - */ - def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin): Either[ChannelException, (Commitments, UpdateAddHtlc)] = { - - val blockCount = Globals.blockCount.get() + * + * @param commitments current commitments + * @param cmd add HTLC command + * @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc) + */ + def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin, blockHeight: Long): Either[ChannelException, (Commitments, UpdateAddHtlc)] = { // our counterparty needs a reasonable amount of time to pull the funds from downstream before we can get refunded (see BOLT 2 and BOLT 11 for a calculation and rationale) - val minExpiry = blockCount + Channel.MIN_CLTV_EXPIRY + val minExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight) if (cmd.cltvExpiry < minExpiry) { - return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockCount = blockCount)) + return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockCount = blockHeight)) } - val maxExpiry = blockCount + Channel.MAX_CLTV_EXPIRY + val maxExpiry = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight) // we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled if (cmd.cltvExpiry >= maxExpiry) { - return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockCount = blockCount)) + return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockCount = blockHeight)) } - if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) { - return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimumMsat, actual = cmd.amountMsat)) + if (cmd.amount < commitments.remoteParams.htlcMinimum) { + return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimum, actual = cmd.amount)) } // let's compute the current commitment *as seen by them* with this change taken into account - val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) // we increment the local htlc index and add an entry to the origins map val commitments1 = addLocalProposal(commitments, add).copy(localNextHtlcId = commitments.localNextHtlcId + 1, originChannels = commitments.originChannels + (add.id -> origin)) // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation @@ -146,8 +174,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.filter(_.direction == IN) - val htlcValueInFlight = UInt64(outgoingHtlcs.map(_.add.amountMsat).sum) - if (htlcValueInFlight > commitments1.remoteParams.maxHtlcValueInFlightMsat) { + // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat) + val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 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)) + } 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 + } else { + return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + } + } + + val htlcValueInFlight = outgoingHtlcs.map(_.add.amountMsat).sum + if (commitments1.remoteParams.maxHtlcValueInFlightMsat < htlcValueInFlight) { // TODO: this should be a specific UPDATE error return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)) } @@ -156,14 +198,6 @@ object Commitments { return Left(TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.remoteParams.maxAcceptedHtlcs)) } - // 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 - val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount else 0 - val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees - if (missing < 0) { - return Left(InsufficientFunds(commitments.channelId, amountMsat = cmd.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.remoteParams.channelReserveSatoshis, feesSatoshis = fees)) - } - Right(commitments1, add) } @@ -172,8 +206,8 @@ object Commitments { throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id) } - if (add.amountMsat < commitments.localParams.htlcMinimumMsat) { - throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimumMsat, actual = add.amountMsat) + if (add.amountMsat < commitments.localParams.htlcMinimum) { + throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimum, actual = add.amountMsat) } // let's compute the current commitment *as seen by us* including this change @@ -181,8 +215,22 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) val incomingHtlcs = reduced.htlcs.filter(_.direction == IN) - val htlcValueInFlight = UInt64(incomingHtlcs.map(_.add.amountMsat).sum) - if (htlcValueInFlight > commitments1.localParams.maxHtlcValueInFlightMsat) { + // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + 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) + if (missingForSender < 0.sat) { + throw InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isFunder) 0.sat else fees) + } else if (missingForReceiver < 0.sat) { + if (commitments.localParams.isFunder) { + throw CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, 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 + } + } + + val htlcValueInFlight = incomingHtlcs.map(_.add.amountMsat).sum + if (commitments1.localParams.maxHtlcValueInFlightMsat < htlcValueInFlight) { throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight) } @@ -190,13 +238,6 @@ object Commitments { throw TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.localParams.maxAcceptedHtlcs) } - // 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 = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw InsufficientFunds(commitments.channelId, amountMsat = add.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) - } - commitments1 } @@ -313,10 +354,10 @@ 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 // we look from remote's point of view, so if local is funder remote doesn't pay the fees - val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + if (missing < 0.sat) { + throw CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees) } (commitments1, fee) @@ -347,10 +388,10 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) // 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 = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount - val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees) + val fees = commitTxFee(commitments1.remoteParams.dustLimit, reduced) + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + if (missing < 0.sat) { + throw CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees) } commitments1 @@ -376,11 +417,12 @@ object Commitments { case Right(remoteNextPerCommitmentPoint) => // remote commitment will includes all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, remoteChanges.acked, localChanges.proposed) - val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeRemoteTxs(keyManager, channelVersion, remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), remoteNextPerCommitmentPoint)) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint)) // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), remoteCommitTx.tx) @@ -424,16 +466,17 @@ object Commitments { // receiving money i.e its commit tx has one output for them val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 1) - val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) - val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath)) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 1) + val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(keyManager, channelVersion, localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec) + val sig = keyManager.sign(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) log.info(s"built local commit number=${localCommit.index + 1} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${localCommitTx.tx.txid} tx={}", spec.htlcs.filter(_.direction == IN).map(_.add.id).mkString(","), spec.htlcs.filter(_.direction == OUT).map(_.add.id).mkString(","), localCommitTx.tx) // TODO: should we have optional sig? (original comment: this tx will NOT be signed if our output is empty) // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) + val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, sig, commit.signature) if (Transactions.checkSpendable(signedCommitTx).isFailure) { throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx) } @@ -442,7 +485,7 @@ object Commitments { if (commit.htlcSignatures.size != sortedHtlcTxs.size) { throw HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size) } - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint)) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) // combine the sigs to make signed txes val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect { @@ -460,8 +503,8 @@ object Commitments { } // we will send our revocation preimage + our next revocation hash - val localPerCommitmentSecret = keyManager.commitmentSecret(localParams.channelKeyPath, commitments.localCommit.index) - val localNextPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index + 2) + val localPerCommitmentSecret = keyManager.commitmentSecret(channelKeyPath, commitments.localCommit.index) + val localNextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index + 2) val revocation = RevokeAndAck( channelId = commitments.channelId, perCommitmentSecret = localPerCommitmentSecret, @@ -521,25 +564,27 @@ object Commitments { } } - def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) + def makeLocalTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } - def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + def makeRemoteTxs(keyManager: KeyManager, channelVersion: ChannelVersion, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey, !localParams.isFunder, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) - val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, Satoshi(remoteParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey, !localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) + val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, spec) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } @@ -574,21 +619,19 @@ object Commitments { def specs2String(commitments: Commitments): String = { s"""specs: |localcommit: - | toLocal: ${commitments.localCommit.spec.toLocalMsat} - | toRemote: ${commitments.localCommit.spec.toRemoteMsat} + | toLocal: ${commitments.localCommit.spec.toLocal} + | toRemote: ${commitments.localCommit.spec.toRemote} | htlcs: |${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")} |remotecommit: - | toLocal: ${commitments.remoteCommit.spec.toLocalMsat} - | toRemote: ${commitments.remoteCommit.spec.toRemoteMsat} + | toLocal: ${commitments.remoteCommit.spec.toLocal} + | toRemote: ${commitments.remoteCommit.spec.toRemote} | htlcs: |${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")} |next remotecommit: - | toLocal: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toLocalMsat).getOrElse("N/A")} - | toRemote: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toRemoteMsat).getOrElse("N/A")} + | toLocal: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toLocal).getOrElse("N/A")} + | toRemote: ${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.toRemote).getOrElse("N/A")} | htlcs: |${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin } -} - - +} \ No newline at end of file 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 5cbe075fcb..cc08ab8ca6 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 @@ -19,18 +19,17 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256} import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin.{OutPoint, _} +import fr.acinq.bitcoin._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.crypto.{Generators, KeyManager} import fr.acinq.eclair.db.ChannelsDb -import fr.acinq.eclair.payment.{Local, Origin} import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, NodeParams, ShortChannelId, addressToPublicKeyScript} +import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _} import scodec.bits.ByteVector import scala.compat.Platform @@ -39,17 +38,17 @@ import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} /** - * Created by PM on 20/05/2016. - */ + * Created by PM on 20/05/2016. + */ object Helpers { /** - * Depending on the state, returns the current temporaryChannelId or channelId - * - * @param stateData - * @return the long identifier of the channel - */ + * Depending on the state, returns the current temporaryChannelId or channelId + * + * @param stateData + * @return the long identifier of the channel + */ def getChannelId(stateData: Data): ByteVector32 = stateData match { case Nothing => ByteVector32.Zeroes case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId @@ -61,11 +60,11 @@ object Helpers { } /** - * We update local/global features at reconnection - * - * @param data - * @return - */ + * We update local/global features at reconnection + * + * @param data + * @return + */ def updateFeatures(data: HasCommitments, localInit: Init, remoteInit: Init): HasCommitments = { val commitments1 = data.commitments.copy( localParams = data.commitments.localParams.copy(globalFeatures = localInit.globalFeatures, localFeatures = localInit.localFeatures), @@ -82,16 +81,16 @@ object Helpers { } /** - * Called by the fundee - */ + * Called by the fundee + */ def validateParamsFundee(nodeParams: NodeParams, open: OpenChannel): Unit = { // 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) throw InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash) - if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS) + if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis >= Channel.MAX_FUNDING) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. - if (open.pushMsat > 1000 * open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis) + if (open.pushMsat > open.fundingSatoshis) throw 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.maxToLocalDelayBlocks) throw ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.maxToLocalDelayBlocks) @@ -107,8 +106,8 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if both to_local and to_remote amounts for the initial commitment // transaction are less than or equal to channel_reserve_satoshis (see BOLT 3). - val (toLocalMsat, toRemoteMsat) = (open.pushMsat, open.fundingSatoshis * 1000 - open.pushMsat) - if (toLocalMsat < open.channelReserveSatoshis * 1000 && toRemoteMsat < open.channelReserveSatoshis * 1000) { + val (toLocalMsat, toRemoteMsat) = (open.pushMsat, open.fundingSatoshis.toMilliSatoshi - open.pushMsat) + if (toLocalMsat < open.channelReserveSatoshis && toRemoteMsat < open.channelReserveSatoshis) { throw ChannelReserveNotMet(open.temporaryChannelId, toLocalMsat, toRemoteMsat, open.channelReserveSatoshis) } @@ -122,13 +121,13 @@ object Helpers { // we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment // now, but it will be done later when we receive `funding_created` - val reserveToFundingRatio = open.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1) + val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio) } /** - * Called by the funder - */ + * Called by the funder + */ def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Unit = { if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) throw InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS) // only enforce dust limit check on mainnet @@ -151,19 +150,19 @@ object Helpers { // MUST reject the channel. Other fields have the same requirements as their counterparts in open_channel. if (open.channelReserveSatoshis < accept.dustLimitSatoshis) throw DustLimitAboveOurChannelReserve(accept.temporaryChannelId, accept.dustLimitSatoshis, open.channelReserveSatoshis) - val reserveToFundingRatio = accept.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1) + val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio) } /** - * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by - * other nodes. - * - * If current update more than [[Channel.REFRESH_CHANNEL_UPDATE_INTERVAL]] old then the delay will be zero. - * - * @param currentUpdateTimestamp - * @return the delay until the next update - */ + * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by + * other nodes. + * + * If current update more than [[Channel.REFRESH_CHANNEL_UPDATE_INTERVAL]] old then the delay will be zero. + * + * @param currentUpdateTimestamp + * @return the delay until the next update + */ def nextChannelUpdateRefresh(currentUpdateTimestamp: Long)(implicit log: LoggingAdapter): FiniteDuration = { val age = Platform.currentTime.milliseconds - currentUpdateTimestamp.seconds val delay = 0.days.max(REFRESH_CHANNEL_UPDATE_INTERVAL - age) @@ -173,56 +172,57 @@ object Helpers { /** * - * @param remoteFeeratePerKw remote fee rate per kiloweight - * @param localFeeratePerKw local fee rate per kiloweight - * @return the "normalized" difference between local and remote fee rate, i.e. |remote - local| / avg(local, remote) + * @param referenceFeePerKw reference fee rate per kiloweight + * @param currentFeePerKw current fee rate per kiloweight + * @return the "normalized" difference between i.e local and remote fee rate: |reference - current| / avg(current, reference) */ - def feeRateMismatch(remoteFeeratePerKw: Long, localFeeratePerKw: Long): Double = - Math.abs((2.0 * (remoteFeeratePerKw - localFeeratePerKw)) / (localFeeratePerKw + remoteFeeratePerKw)) + def feeRateMismatch(referenceFeePerKw: Long, currentFeePerKw: Long): Double = + Math.abs((2.0 * (referenceFeePerKw - currentFeePerKw)) / (currentFeePerKw + referenceFeePerKw)) def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean = feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio /** * - * @param remoteFeeratePerKw remote fee rate per kiloweight - * @param localFeeratePerKw local fee rate per kiloweight + * @param referenceFeePerKw reference fee rate per kiloweight + * @param currentFeePerKw current fee rate per kiloweight * @param maxFeerateMismatchRatio maximum fee rate mismatch ratio - * @return true if the difference between local and remote fee rates is too high. - * the actual check is |remote - local| / avg(local, remote) > mismatch ratio + * @return true if the difference between current and reference fee rates is too high. + * the actual check is |reference - current| / avg(current, reference) > mismatch ratio */ - def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = - feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio + def isFeeDiffTooHigh(referenceFeePerKw: Long, currentFeePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = + feeRateMismatch(referenceFeePerKw, currentFeePerKw) > maxFeerateMismatchRatio /** - * - * @param remoteFeeratePerKw remote fee rate per kiloweight - * @return true if the remote fee rate is too small - */ + * + * @param remoteFeeratePerKw remote fee rate per kiloweight + * @return true if the remote fee rate is too small + */ def isFeeTooSmall(remoteFeeratePerKw: Long): Boolean = { remoteFeeratePerKw < fr.acinq.eclair.MinimumFeeratePerKw } def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId) = { val features = ByteVector.empty // empty features for now - val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(commitments.localParams.channelKeyPath, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) + val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) + val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey.path, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) } /** - * This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words, - * this tells if we can use the channel to make a payment. - * - */ + * This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words, + * this tells if we can use the channel to make a payment. + * + */ def aboveReserve(commitments: Commitments)(implicit log: LoggingAdapter): Boolean = { val remoteCommit = commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit case _ => commitments.remoteCommit } - val toRemoteSatoshis = remoteCommit.spec.toRemoteMsat / 1000 + val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi // NB: this is an approximation (we don't take network fees into account) - val result = toRemoteSatoshis > commitments.remoteParams.channelReserveSatoshis - log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserveSatoshis} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") + val result = toRemoteSatoshis > commitments.remoteParams.channelReserve + log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") result } @@ -242,37 +242,39 @@ object Helpers { } /** - * Creates both sides's first commitment transaction - * - * @param localParams - * @param remoteParams - * @param pushMsat - * @param fundingTxHash - * @param fundingTxOutputIndex - * @param remoteFirstPerCommitmentPoint - * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) - */ - def makeFirstCommitTxs(keyManager: KeyManager, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { - val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat - val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat - - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toLocalMsat, toRemoteMsat = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toRemoteMsat, toRemoteMsat = toLocalMsat) + * Creates both sides's first commitment transaction + * + * @param localParams + * @param remoteParams + * @param pushMsat + * @param fundingTxHash + * @param fundingTxOutputIndex + * @param remoteFirstPerCommitmentPoint + * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) + */ + def makeFirstCommitTxs(keyManager: KeyManager, channelVersion: ChannelVersion, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: Long, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = { + val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat + val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat + + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) if (!localParams.isFunder) { // they are funder, therefore they pay the fee: we need to make sure they can afford it! - val toRemoteMsat = remoteSpec.toLocalMsat - val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec).amount - val missing = toRemoteMsat / 1000 - localParams.channelReserveSatoshis - fees - if (missing < 0) { - throw CannotAffordFees(temporaryChannelId, missingSatoshis = -1 * missing, reserveSatoshis = localParams.channelReserveSatoshis, feesSatoshis = fees) + val toRemoteMsat = remoteSpec.toLocal + val fees = commitTxFee(remoteParams.dustLimit, remoteSpec) + val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees + if (missing < Satoshi(0)) { + throw CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees) } } - val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, 0) - val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) - val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) + val (localCommitTx, _, _) = Commitments.makeLocalTxs(keyManager, channelVersion, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) (localSpec, localCommitTx, remoteSpec, remoteCommitTx) } @@ -280,14 +282,14 @@ object Helpers { } /** - * Tells whether or not their expected next remote commitment number matches with our data - * - * @param d - * @param nextRemoteRevocationNumber - * @return - * - true if parties are in sync or remote is behind - * - false if we are behind - */ + * Tells whether or not their expected next remote commitment number matches with our data + * + * @param d + * @param nextRemoteRevocationNumber + * @return + * - true if parties are in sync or remote is behind + * - false if we are behind + */ def checkLocalCommit(d: HasCommitments, nextRemoteRevocationNumber: Long): Boolean = { if (d.commitments.localCommit.index == nextRemoteRevocationNumber) { // they just sent a new commit_sig, we have received it but they didn't receive our revocation @@ -305,14 +307,14 @@ object Helpers { } /** - * Tells whether or not their expected next local commitment number matches with our data - * - * @param d - * @param nextLocalCommitmentNumber - * @return - * - true if parties are in sync or remote is behind - * - false if we are behind - */ + * Tells whether or not their expected next local commitment number matches with our data + * + * @param d + * @param nextLocalCommitmentNumber + * @return + * - true if parties are in sync or remote is behind + * - false if we are behind + */ def checkRemoteCommit(d: HasCommitments, nextLocalCommitmentNumber: Long): Boolean = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if nextLocalCommitmentNumber == waitingForRevocation.nextRemoteCommit.index => @@ -351,28 +353,28 @@ object Helpers { // @formatter:on /** - * Indicates whether local has anything at stake in this channel - * - * @param data - * @return true if channel was never open, or got closed immediately, had never any htlcs and local never had a positive balance - */ + * Indicates whether local has anything at stake in this channel + * + * @param data + * @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 = data.commitments.localCommit.index == 0 && - data.commitments.localCommit.spec.toLocalMsat == 0 && + data.commitments.localCommit.spec.toLocal == 0.msat && data.commitments.remoteCommit.index == 0 && - data.commitments.remoteCommit.spec.toRemoteMsat == 0 && + data.commitments.remoteCommit.spec.toRemote == 0.msat && data.commitments.remoteNextCommitInfo.isRight /** - * As soon as a tx spending the funding tx has reached min_depth, we know what the closing type will be, before - * the whole closing process finishes(e.g. there may still be delayed or unconfirmed child transactions). It can - * save us from attempting to publish some transactions. - * - * Note that we can't tell for mutual close before it is already final, because only one tx needs to be confirmed. - * - * @param closing channel state data - * @return the channel closing type, if applicable - */ + * As soon as a tx spending the funding tx has reached min_depth, we know what the closing type will be, before + * the whole closing process finishes(e.g. there may still be delayed or unconfirmed child transactions). It can + * save us from attempting to publish some transactions. + * + * Note that we can't tell for mutual close before it is already final, because only one tx needs to be confirmed. + * + * @param closing channel state data + * @return the channel closing type, if applicable + */ def isClosingTypeAlreadyKnown(closing: DATA_CLOSING): Option[ClosingType] = closing match { case _ if closing.localCommitPublished.exists(lcp => lcp.irrevocablySpent.values.toSet.contains(lcp.commitTx.txid)) => Some(LocalClose) @@ -388,13 +390,13 @@ object Helpers { } /** - * Checks if a channel is closed (i.e. its closing tx has been confirmed) - * - * @param data channel state data - * @param additionalConfirmedTx_opt additional confirmed transaction; we need this for the mutual close scenario - * because we don't store the closing tx in the channel state - * @return the channel closing type, if applicable - */ + * Checks if a channel is closed (i.e. its closing tx has been confirmed) + * + * @param data channel state data + * @param additionalConfirmedTx_opt additional confirmed transaction; we need this for the mutual close scenario + * 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 { case closing: DATA_CLOSING if additionalConfirmedTx_opt.exists(closing.mutualClosePublished.contains) => Some(MutualClose) @@ -453,10 +455,10 @@ object Helpers { require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey") log.debug(s"making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments)) // TODO: check that - val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis)) + val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath)) - val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath)) + val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig) log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") (closingTx, closingSigned) @@ -464,20 +466,20 @@ object Helpers { def checkClosingSignature(keyManager: KeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Try[Transaction] = { import commitments._ - val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount.amount).sum - if (remoteClosingFee.amount > lastCommitFeeSatoshi) { - log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.amount} lastCommitFeeSatoshi=$lastCommitFeeSatoshi") - throw InvalidCloseFee(commitments.channelId, remoteClosingFee.amount) + val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount).sum + if (remoteClosingFee > lastCommitFeeSatoshi) { + log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.toLong} lastCommitFeeSatoshi=$lastCommitFeeSatoshi") + throw InvalidCloseFee(commitments.channelId, remoteClosingFee) } val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx).recover { case _ => throw InvalidCloseSignature(commitments.channelId, signedClosingTx.tx) } } def generateTx(desc: String)(attempt: Try[TransactionWithInputInfo])(implicit log: LoggingAdapter): Option[TransactionWithInputInfo] = { attempt match { case Success(txinfo) => - log.info(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount.amount).sum} tx=${txinfo.tx}") + log.info(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount).sum} tx=${txinfo.tx}") Some(txinfo) case Failure(t: TxGenerationSkipped) => log.info(s"tx generation skipped: desc=$desc reason: ${t.getMessage}") @@ -489,27 +491,27 @@ object Helpers { } /** - * - * 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 HTLC that we can claim) - */ + * + * 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 HTLC that we can claim) + */ def claimCurrentLocalCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") - - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(localParams.channelKeyPath).publicKey, localPerCommitmentPoint) + val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output")(Try { - val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) @@ -539,12 +541,12 @@ object Helpers { generateTx("claim-htlc-delayed")(Try { val claimDelayed = Transactions.makeClaimDelayedOutputTx( txinfo.tx, - Satoshi(localParams.dustLimitSatoshis), + localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed) - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(localParams.channelKeyPath), localPerCommitmentPoint) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint) Transactions.addSigs(claimDelayed, sig) }) } @@ -559,24 +561,24 @@ object Helpers { } /** - * - * Claim all the HTLCs that we've received from their current commit tx - * - * @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 HTLC that we can claim) - */ + * + * Claim all the HTLCs that we've received from their current commit tx + * + * @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 HTLC that we can claim) + */ def claimRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - import commitments.{commitInput, localParams, remoteParams} + import commitments.{channelVersion, commitInput, localParams, remoteParams} require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") - val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) + val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, channelVersion, remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -590,9 +592,9 @@ object Helpers { // incoming htlc for which we have the preimage: we spend it directly case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try { val preimage = preimages.find(r => sha256(r) == add.paymentHash).get - val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) + val txinfo = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig, preimage) }) @@ -600,9 +602,9 @@ object Helpers { // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try { - val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, Satoshi(localParams.dustLimitSatoshis), localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) + val txinfo = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputsAlreadyUsed, localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc) outputsAlreadyUsed = outputsAlreadyUsed + txinfo.input.outPoint.index.toInt - val sig = keyManager.sign(txinfo, keyManager.htlcPoint(localParams.channelKeyPath), remoteCommit.remotePerCommitmentPoint) + val sig = keyManager.sign(txinfo, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint) Transactions.addSigs(txinfo, sig) }) }.toSeq.flatten @@ -614,25 +616,26 @@ object Helpers { } /** - * - * 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 a list of transactions (one per HTLC that we can claim) - */ + * + * 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 a list of transactions (one per HTLC that we can claim) + */ def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(commitments.localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(commitments.localParams.dustLimitSatoshis), + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(commitments.localParams.channelKeyPath), remotePerCommitmentPoint) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) @@ -646,20 +649,22 @@ object Helpers { } /** - * 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 - */ + * 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: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ require(tx.txIn.size == 1, "commitment tx should have 1 input") + //val fundingPubKey = commitments.localParams.fundingPubKey(keyManager) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) 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 @@ -668,9 +673,9 @@ object Helpers { .map { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) @@ -679,15 +684,15 @@ object Helpers { // first we will claim our main output right away val mainTx = generateTx("claim-p2wpkh-output")(Try { - val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(localParams.channelKeyPath), remotePerCommitmentPoint) + val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPubkey, sig) }) // then we punish them by stealing their main output val mainPenaltyTx = generateTx("main-penalty")(Try { - val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val txinfo = Transactions.makeMainPenaltyTx(tx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) Transactions.addSigs(txinfo, sig) }) @@ -703,15 +708,15 @@ object Helpers { // and finally we steal the htlc outputs var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs - val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - generateTx("htlc-penalty")(Try { - val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) - outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) - }) - }.toList.flatten + val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) + generateTx("htlc-penalty")(Try { + val htlcPenalty = Transactions.makeHtlcPenaltyTx(tx, outputsAlreadyUsed, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) + outputsAlreadyUsed = outputsAlreadyUsed + htlcPenalty.input.outPoint.index.toInt + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + }) + }.toList.flatten RevokedCommitPublished( commitTx = tx, @@ -725,21 +730,21 @@ object Helpers { } /** - * 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 txes 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. - * - * @param keyManager - * @param commitments - * @param revokedCommitPublished - * @param htlcTx - * @return - */ + * 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 txes 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. + * + * @param keyManager + * @param commitments + * @param revokedCommitPublished + * @param htlcTx + * @return + */ def claimRevokedHtlcTxOutputs(keyManager: KeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Option[Transaction]) = { if (htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && !(revokedCommitPublished.claimMainOutputTx ++ revokedCommitPublished.mainPenaltyTx ++ revokedCommitPublished.htlcPenaltyTxs).map(_.txid).toSet.contains(htlcTx.txid)) { @@ -748,22 +753,23 @@ object Helpers { import commitments._ val tx = revokedCommitPublished.commitTx val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime) + val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) // 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)) .flatMap { remotePerCommitmentSecret => val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) // we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) generateTx("claim-htlc-delayed-penalty")(Try { - val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret) + val htlcDelayedPenalty = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret) 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) @@ -782,18 +788,18 @@ object Helpers { } /** - * In CLOSING state, any time we see a new transaction, we try to extract a preimage from it in order to fulfill the - * corresponding incoming htlc in an upstream channel. - * - * Not doing that would result in us losing money, because the downstream node would pull money from one side, and - * the upstream node would get refunded after a timeout. - * - * @param localCommit - * @param tx - * @return a set of pairs (add, fulfills) if extraction was successful: - * - add is the htlc in the downstream channel from which we extracted the preimage - * - fulfill needs to be sent to the upstream channel - */ + * In CLOSING state, any time we see a new transaction, we try to extract a preimage from it in order to fulfill the + * corresponding incoming htlc in an upstream channel. + * + * Not doing that would result in us losing money, because the downstream node would pull money from one side, and + * the upstream node would get refunded after a timeout. + * + * @param localCommit + * @param tx + * @return a set of pairs (add, fulfills) if extraction was successful: + * - add is the htlc in the downstream channel from which we extracted the preimage + * - fulfill needs to be sent to the upstream channel + */ def extractPreimages(localCommit: LocalCommit, tx: Transaction)(implicit log: LoggingAdapter): Set[(UpdateAddHtlc, UpdateFulfillHtlc)] = { val paymentPreimages = tx.txIn.map(_.witness match { case ScriptWitness(Seq(localSig, paymentPreimage, htlcOfferedScript)) if paymentPreimage.size == 32 => @@ -819,14 +825,14 @@ object Helpers { } /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or - * more htlcs have timed out and need to be failed in an upstream channel. - * - * @param localCommit - * @param localDustLimit - * @param tx a tx that has reached mindepth - * @return a set of htlcs that need to be failed upstream - */ + * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or + * more htlcs have timed out and need to be failed in an upstream channel. + * + * @param localCommit + * @param localDustLimit + * @param tx a tx that has reached mindepth + * @return a set of htlcs that need to be failed upstream + */ def timedoutHtlcs(localCommit: LocalCommit, localDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == localCommit.publishableTxs.commitTx.tx.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) @@ -843,14 +849,14 @@ object Helpers { } /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or - * more htlcs have timed out and need to be failed in an upstream channel. - * - * @param remoteCommit - * @param remoteDustLimit - * @param tx a tx that has reached mindepth - * @return a set of htlcs that need to be failed upstream - */ + * In CLOSING state, when we are notified that a transaction has been confirmed, we analyze it to find out if one or + * more htlcs have timed out and need to be failed in an upstream channel. + * + * @param remoteCommit + * @param remoteDustLimit + * @param tx a tx that has reached mindepth + * @return a set of htlcs that need to be failed upstream + */ def timedoutHtlcs(remoteCommit: RemoteCommit, remoteDustLimit: Satoshi, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (tx.txid == remoteCommit.txid) { // the tx is a commitment tx, we can immediately fail all dust htlcs (they don't have an output in the tx) @@ -867,14 +873,14 @@ object Helpers { } /** - * As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether - * or not they actually have an output in the commitment tx). - * - * @param localCommit - * @param remoteCommit - * @param nextRemoteCommit_opt - * @param tx a transaction that is sufficiently buried in the blockchain - */ + * As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether + * or not they actually have an output in the commitment tx). + * + * @param localCommit + * @param remoteCommit + * @param nextRemoteCommit_opt + * @param tx a transaction that is sufficiently buried in the blockchain + */ def onchainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = { if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) { localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add) @@ -888,17 +894,17 @@ object Helpers { } /** - * If a local commitment tx reaches min_depth, we need to fail the outgoing htlcs that only us had signed, because - * they will never reach the blockchain. - * - * Those are only present in the remote's commitment. - * - * @param localCommit - * @param remoteCommit - * @param tx - * @param log - * @return - */ + * If a local commitment tx reaches min_depth, we need to fail the outgoing htlcs that only us had signed, because + * they will never reach the blockchain. + * + * Those are only present in the remote's commitment. + * + * @param localCommit + * @param remoteCommit + * @param tx + * @param log + * @return + */ def overriddenOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) { // our commit got confirmed, so any htlc that we signed but they didn't sign will never reach the chain @@ -922,17 +928,17 @@ object Helpers { } else Set.empty /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the - * local commit scenario and keep track of it. - * - * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be - * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't - * want to wait forever before declaring that the channel is CLOSED. - * - * @param localCommitPublished - * @param tx a transaction that has been irrevocably confirmed - * @return - */ + * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the + * local commit scenario and keep track of it. + * + * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be + * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't + * want to wait forever before declaring that the channel is CLOSED. + * + * @param localCommitPublished + * @param tx a transaction that has been irrevocably confirmed + * @return + */ def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction) = { // even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant @@ -951,17 +957,17 @@ object Helpers { } /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the - * remote commit scenario and keep track of it. - * - * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be - * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't - * want to wait forever before declaring that the channel is CLOSED. - * - * @param remoteCommitPublished - * @param tx a transaction that has been irrevocably confirmed - * @return - */ + * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the + * remote commit scenario and keep track of it. + * + * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be + * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't + * want to wait forever before declaring that the channel is CLOSED. + * + * @param remoteCommitPublished + * @param tx a transaction that has been irrevocably confirmed + * @return + */ def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction) = { // even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant @@ -977,17 +983,17 @@ object Helpers { } /** - * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the - * revoked commit scenario and keep track of it. - * - * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be - * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't - * want to wait forever before declaring that the channel is CLOSED. - * - * @param revokedCommitPublished - * @param tx a transaction that has been irrevocably confirmed - * @return - */ + * In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the + * revoked commit scenario and keep track of it. + * + * We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be + * spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't + * want to wait forever before declaring that the channel is CLOSED. + * + * @param revokedCommitPublished + * @param tx a transaction that has been irrevocably confirmed + * @return + */ def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction) = { // even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant @@ -1006,13 +1012,13 @@ object Helpers { } /** - * A local commit is considered done when: - * - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours) - * - all 3rd stage txes (txes spending htlc txes) have been confirmed - * - * @param localCommitPublished - * @return - */ + * A local commit is considered done when: + * - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours) + * - all 3rd stage txes (txes spending htlc txes) have been confirmed + * + * @param localCommitPublished + * @return + */ def isLocalCommitDone(localCommitPublished: LocalCommitPublished) = { // is the commitment tx buried? (we need to check this because we may not have any outputs) val isCommitTxConfirmed = localCommitPublished.irrevocablySpent.values.toSet.contains(localCommitPublished.commitTx.txid) @@ -1027,12 +1033,12 @@ object Helpers { } /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - * - * @param remoteCommitPublished - * @return - */ + * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed + * (even if the spending tx was not ours). + * + * @param remoteCommitPublished + * @return + */ def isRemoteCommitDone(remoteCommitPublished: RemoteCommitPublished) = { // is the commitment tx buried? (we need to check this because we may not have any outputs) val isCommitTxConfirmed = remoteCommitPublished.irrevocablySpent.values.toSet.contains(remoteCommitPublished.commitTx.txid) @@ -1043,12 +1049,12 @@ object Helpers { } /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - * - * @param revokedCommitPublished - * @return - */ + * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed + * (even if the spending tx was not ours). + * + * @param revokedCommitPublished + * @return + */ def isRevokedCommitDone(revokedCommitPublished: RevokedCommitPublished) = { // is the commitment tx buried? (we need to check this because we may not have any outputs) val isCommitTxConfirmed = revokedCommitPublished.irrevocablySpent.values.toSet.contains(revokedCommitPublished.commitTx.txid) @@ -1063,17 +1069,17 @@ object Helpers { } /** - * This helper function tells if the utxo consumed by the given transaction has already been irrevocably spent (possibly by this very transaction) - * - * It can be useful to: - * - not attempt to publish this tx when we know this will fail - * - not watch for confirmations if we know the tx is already confirmed - * - not watch the corresponding utxo when we already know the final spending tx - * - * @param tx a tx with only one input - * @param irrevocablySpent a map of known spent outpoints - * @return true if we know for sure that the utxos consumed by the tx have already irrevocably been spent, false otherwise - */ + * This helper function tells if the utxo consumed by the given transaction has already been irrevocably spent (possibly by this very transaction) + * + * It can be useful to: + * - not attempt to publish this tx when we know this will fail + * - not watch for confirmations if we know the tx is already confirmed + * - not watch the corresponding utxo when we already know the final spending tx + * + * @param tx a tx with only one input + * @param irrevocablySpent a map of known spent outpoints + * @return true if we know for sure that the utxos consumed by the tx have already irrevocably been spent, false otherwise + */ def inputsAlreadySpent(tx: Transaction, irrevocablySpent: Map[OutPoint, ByteVector32]): Boolean = { require(tx.txIn.size == 1, "only tx with one input is supported") val outPoint = tx.txIn.head.outPoint @@ -1081,14 +1087,14 @@ object Helpers { } /** - * This helper function returns the fee paid by the given transaction. - * - * It relies on the current channel data to find the parent tx and compute the fee, and also provides a description. - * - * @param tx a tx for which we want to compute the fee - * @param d current channel data - * @return if the parent tx is found, a tuple (fee, description) - */ + * This helper function returns the fee paid by the given transaction. + * + * It relies on the current channel data to find the parent tx and compute the fee, and also provides a description. + * + * @param tx a tx for which we want to compute the fee + * @param d current channel data + * @return if the parent tx is found, a tuple (fee, description) + */ def networkFeePaid(tx: Transaction, d: DATA_CLOSING): Option[(Satoshi, String)] = { // only funder pays the fee if (d.commitments.localParams.isFunder) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala index f96cb9d62a..9745d319c0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala @@ -16,10 +16,14 @@ package fr.acinq.eclair.crypto +import java.io.ByteArrayInputStream +import java.nio.ByteOrder + import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.channel.{ChannelVersion, LocalParams} import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo import scodec.bits.ByteVector @@ -28,7 +32,7 @@ trait KeyManager { def nodeId: PublicKey - def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey + def fundingPublicKey(keyPath: DeterministicWallet.KeyPath): ExtendedPublicKey def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey @@ -42,6 +46,23 @@ trait KeyManager { def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PublicKey + def channelKeyPath(localParams: LocalParams, channelVersion: ChannelVersion): DeterministicWallet.KeyPath = if (channelVersion.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) { + // deterministic mode: use the funding pubkey to compute the channel key path + KeyManager.channelKeyPath(fundingPublicKey(localParams.fundingKeyPath)) + } else { + // legacy mode: we reuse the funding key path as our channel key path + localParams.fundingKeyPath + } + + /** + * + * @param isFunder true if we're funding this channel + * @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 + /** * * @param tx input transaction @@ -73,5 +94,38 @@ trait KeyManager { */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey): ByteVector64 - def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + /** + * Sign a channel announcement message + * + * @param fundingKeyPath BIP32 path of the funding public key + * @param chainHash chain hash + * @param shortChannelId short channel id + * @param remoteNodeId remote node id + * @param remoteFundingKey remote funding pubkey + * @param features channel features + * @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's + * private key, bitcoinSig is the signature of the channel announcement with our funding private key + */ + def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) +} + +object KeyManager { + /** + * Create a BIP32 path from a public key. This path will be used to derive channel keys. + * Having channel keys derived from the funding public keys makes it very easy to retrieve your funds when've you've lost your data: + * - connect to your peer and use DLP to get them to publish their remote commit tx + * - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data + * - recompute your channel keys and spend your output + * + * @param fundingPubKey funding public key + * @return a BIP32 path + */ + def channelKeyPath(fundingPubKey: PublicKey) : DeterministicWallet.KeyPath = { + val buffer = Crypto.sha256(fundingPubKey.value) + val bis = new ByteArrayInputStream(buffer.toArray) + def next() = Protocol.uint32(bis, ByteOrder.BIG_ENDIAN) + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next())) + } + + def channelKeyPath(fundingPubKey: DeterministicWallet.ExtendedPublicKey) : DeterministicWallet.KeyPath = channelKeyPath(fundingPubKey.publicKey) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index 2bf80bccc4..d7945b71ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -17,13 +17,13 @@ package fr.acinq.eclair.crypto import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey} +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.eclair.ShortChannelId import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo +import fr.acinq.eclair.{ShortChannelId, secureRandom} import scodec.bits.ByteVector object LocalKeyManager { @@ -80,6 +80,12 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath) = 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) + def next() = secureRandom.nextInt() & 0xFFFFFFFFL + DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last)) + } + override def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(0))) override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1))) @@ -137,9 +143,9 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana Transactions.sign(tx, currentKey) } - override def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { + override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { val localNodeSecret = nodeKey.privateKey - val localFundingPrivKey = fundingPrivateKey(channelKeyPath).privateKey + val localFundingPrivKey = privateKeys.get(fundingKeyPath).privateKey Announcements.signChannelAnnouncement(chainHash, shortChannelId, localNodeSecret, remoteNodeId, localFundingPrivKey, remoteFundingKey, features) } } 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 7142924b7a..ddeaec963b 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 @@ -318,12 +318,16 @@ object Sphinx extends Logging { * @return an encrypted failure packet that can be sent to the destination node. */ def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { - require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + if (packet.length != PacketLength) { + logger.warn(s"invalid error packet length ${packet.length}, must be $PacketLength (malicious or buggy downstream node)") + } val key = generateKey("ammag", sharedSecret) val stream = generateStream(key, PacketLength) logger.debug(s"ammag key: $key") logger.debug(s"error stream: $stream") - packet xor stream + // If we received a packet with an invalid length, we trim and pad to forward a packet with a normal length upstream. + // This is a poor man's attempt at increasing the likelihood of the sender receiving the error. + packet.take(PacketLength).padLeft(PacketLength) xor stream } /** 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 63dbe4d74f..c271604353 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,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} @@ -35,7 +35,7 @@ trait AuditDb { def add(networkFeePaid: NetworkFeePaid) - def add(channelErrorOccured: ChannelErrorOccured) + def add(channelErrorOccurred: ChannelErrorOccurred) def listSent(from: Long, to: Long): Seq[PaymentSent] @@ -47,12 +47,12 @@ trait AuditDb { def stats: Seq[Stats] - def close: Unit + def close(): Unit } -case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacitySat: Long, isFunder: Boolean, isPrivate: Boolean, event: String) +case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isFunder: Boolean, isPrivate: Boolean, event: String) -case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, feeSat: Long, txType: String, timestamp: Long) +case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: Long) -case class Stats(channelId: ByteVector32, avgPaymentAmountSatoshi: Long, paymentCount: Int, relayFeeSatoshi: Long, networkFeeSatoshi: Long) +case class Stats(channelId: ByteVector32, avgPaymentAmount: Satoshi, paymentCount: Int, relayFee: Satoshi, networkFee: Satoshi) 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 dd9fb9e120..16a7213a84 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.CltvExpiry import fr.acinq.eclair.channel.HasCommitments trait ChannelsDb { @@ -27,9 +28,9 @@ trait ChannelsDb { def listLocalChannels(): Seq[HasCommitments] - def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: Long) + def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry) - def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, Long)] + def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] def close(): 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 eb982b0f7e..75f9a1404b 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 @@ -19,6 +19,8 @@ package fr.acinq.eclair.db import java.io.File import java.sql.{Connection, DriverManager} +import fr.acinq.bitcoin.Block +import fr.acinq.eclair.NodeParams import fr.acinq.eclair.db.sqlite._ trait Databases { @@ -56,7 +58,7 @@ object Databases { } def databaseByConnections(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection) = new Databases { - override val network = new SqliteNetworkDb(networkJdbc) + override val network = new SqliteNetworkDb(networkJdbc, Block.RegtestGenesisBlock.hash) override val audit = new SqliteAuditDb(auditJdbc) override val channels = new SqliteChannelsDb(eclairJdbc) override val peers = new SqlitePeersDb(eclairJdbc) 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 546516785f..bd72a235ad 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 @@ -19,8 +19,11 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.router.PublicChannel import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} +import scala.collection.immutable.SortedMap + trait NetworkDb { def addNode(n: NodeAnnouncement) @@ -35,22 +38,13 @@ trait NetworkDb { def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi) - def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Seq(shortChannelId)) - - /** - * This method removes channel announcements and associated channel updates for a list of channel ids - * - * @param shortChannelIds list of short channel ids - */ - def removeChannels(shortChannelIds: Iterable[ShortChannelId]) + def updateChannel(u: ChannelUpdate) - def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)] + def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Set(shortChannelId)) - def addChannelUpdate(u: ChannelUpdate) - - def updateChannelUpdate(u: ChannelUpdate) + def removeChannels(shortChannelIds: Iterable[ShortChannelId]) - def listChannelUpdates(): Seq[ChannelUpdate] + def listChannels(): SortedMap[ShortChannelId, PublicChannel] def addToPruned(shortChannelIds: Iterable[ShortChannelId]): 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 b13e190446..e7113eb37d 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 @@ -17,70 +17,176 @@ package fr.acinq.eclair.db import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.Hop +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} + +import scala.compat.Platform trait PaymentsDb { - // creates a record for a non yet finalized outgoing payment - def addOutgoingPayment(outgoingPayment: OutgoingPayment) + /** Create a record for a non yet finalized outgoing payment. */ + def addOutgoingPayment(outgoingPayment: OutgoingPayment): Unit - // updates the status of the payment, if the newStatus is SUCCEEDED you must supply a preimage - def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value, preimage: Option[ByteVector32] = None) + /** Update the status of the payment in case of success. */ + def updateOutgoingPayment(paymentResult: PaymentSent): Unit + /** Update the status of the payment in case of failure. */ + def updateOutgoingPayment(paymentResult: PaymentFailed): Unit + + /** Get an outgoing payment attempt. */ def getOutgoingPayment(id: UUID): Option[OutgoingPayment] - // all the outgoing payment (attempts) to pay the given paymentHash - def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] + /** List all the outgoing payment attempts that are children of the given id. */ + def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] - def listOutgoingPayments(): Seq[OutgoingPayment] + /** List all the outgoing payment attempts that tried to pay the given payment hash. */ + def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] - def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) + /** List all the outgoing payment attempts in the given time range (milli-seconds). */ + def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] - def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] + /** Add a new expected incoming payment (not yet received). */ + def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32): Unit - // returns non paid payment request - def getPendingPaymentRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + /** + * Mark an incoming payment as received (paid). The received amount may exceed the payment request amount. + * Note that this function assumes that there is a matching payment request in the DB. + */ + def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long = Platform.currentTime): Unit - def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] + /** Get information about the incoming payment (paid or not) for the given payment hash, if any. */ + def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] - // returns non paid, non expired payment requests - def listPendingPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] + /** List all incoming payments (pending, expired and succeeded) in the given time range (milli-seconds). */ + def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] - // assumes there is already a payment request for it (the record for the given payment hash) - def addIncomingPayment(payment: IncomingPayment) + /** List all pending (not paid, not expired) incoming payments in the given time range (milli-seconds). */ + def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] - def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] + /** List all expired (not paid) incoming payments in the given time range (milli-seconds). */ + def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] - def listIncomingPayments(): Seq[IncomingPayment] + /** List all received (paid) incoming payments in the given time range (milli-seconds). */ + def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] } /** - * Incoming payment object stored in DB. - * - * @param paymentHash identifier of the payment - * @param amountMsat amount of the payment, in milli-satoshis - * @param receivedAt absolute time in seconds since UNIX epoch when the payment was received. - */ -case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, receivedAt: Long) + * 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). + * + * @param paymentRequest Bolt 11 payment request. + * @param paymentPreimage pre-image associated with the payment request's payment_hash. + * @param createdAt absolute time in milli-seconds since UNIX epoch when the payment request was generated. + * @param status current status of the payment. + */ +case class IncomingPayment(paymentRequest: PaymentRequest, + paymentPreimage: ByteVector32, + createdAt: Long, + status: IncomingPaymentStatus) + +sealed trait IncomingPaymentStatus + +object IncomingPaymentStatus { + + /** Payment is pending (waiting to receive). */ + case object Pending extends IncomingPaymentStatus + + /** Payment has expired. */ + case object Expired extends IncomingPaymentStatus + + /** + * Payment has been successfully received. + * + * @param amount amount of the payment received, in milli-satoshis (may exceed the payment request amount). + * @param receivedAt absolute time in milli-seconds since UNIX epoch when the payment was received. + */ + case class Received(amount: MilliSatoshi, receivedAt: Long) extends IncomingPaymentStatus + +} /** - * Sent payment is every payment that is sent by this node, they may not be finalized and - * when is final it can be failed or successful. - * - * @param id internal payment identifier - * @param paymentHash payment_hash - * @param preimage the preimage of the payment_hash, known if the outgoing payment was successful - * @param amountMsat amount of the payment, in milli-satoshis - * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. - * @param completedAt absolute time in seconds since UNIX epoch when the payment succeeded. - * @param status current status of the payment. - */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amountMsat: Long, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value) - -object OutgoingPaymentStatus extends Enumeration { - val PENDING = Value(1, "PENDING") - val SUCCEEDED = Value(2, "SUCCEEDED") - val FAILED = Value(3, "FAILED") + * An outgoing payment sent by this node. + * At first it is in a pending state, then will become either a success or a failure. + * + * @param id internal payment identifier. + * @param parentId internal identifier of a parent payment, or [[id]] if single-part payment. + * @param externalId external payment identifier: lets lightning applications reconcile payments with their own db. + * @param paymentHash payment_hash. + * @param amount amount of the payment, in milli-satoshis. + * @param targetNodeId node ID of the payment 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 status current status of the payment. + */ +case class OutgoingPayment(id: UUID, + parentId: UUID, + externalId: Option[String], + paymentHash: ByteVector32, + amount: MilliSatoshi, + targetNodeId: PublicKey, + createdAt: Long, + paymentRequest: Option[PaymentRequest], + status: OutgoingPaymentStatus) + +sealed trait OutgoingPaymentStatus + +object OutgoingPaymentStatus { + + /** Payment is pending (waiting for the recipient to release the pre-image). */ + case object Pending extends OutgoingPaymentStatus + + /** + * Payment has been successfully sent and the recipient released the pre-image. + * We now have a valid proof-of-payment. + * + * @param paymentPreimage the preimage of the payment_hash. + * @param feesPaid total amount of fees paid to intermediate routing nodes. + * @param route payment route. + * @param completedAt absolute time in milli-seconds since UNIX epoch when the payment was completed. + */ + case class Succeeded(paymentPreimage: ByteVector32, feesPaid: MilliSatoshi, route: Seq[HopSummary], completedAt: Long) extends OutgoingPaymentStatus + + /** + * Payment has failed and may be retried. + * + * @param failures failed payment attempts. + * @param completedAt absolute time in milli-seconds since UNIX epoch when the payment was completed. + */ + case class Failed(failures: Seq[FailureSummary], completedAt: Long) extends OutgoingPaymentStatus + +} + +/** A minimal representation of a hop in a payment route (suitable to store in a database). */ +case class HopSummary(nodeId: PublicKey, nextNodeId: PublicKey, shortChannelId: Option[ShortChannelId] = None) { + override def toString = shortChannelId match { + case Some(shortChannelId) => s"$nodeId->$nextNodeId ($shortChannelId)" + case None => s"$nodeId->$nextNodeId" + } +} + +object HopSummary { + def apply(h: Hop): HopSummary = HopSummary(h.nodeId, h.nextNodeId, Some(h.lastUpdate.shortChannelId)) +} + +/** A minimal representation of a payment failure (suitable to store in a database). */ +case class FailureSummary(failureType: FailureType.Value, failureMessage: String, failedRoute: List[HopSummary]) + +object FailureType extends Enumeration { + val LOCAL = Value(1, "Local") + val REMOTE = Value(2, "Remote") + val UNREADABLE_REMOTE = Value(3, "UnreadableRemote") +} + +object FailureSummary { + def apply(f: PaymentFailure): FailureSummary = f match { + case LocalFailure(t) => FailureSummary(FailureType.LOCAL, t.getMessage, Nil) + case RemoteFailure(route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList) + case UnreadableRemoteFailure(route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList) + } } \ No newline at end of file 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 519ed96b8d..ad49eca9e4 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 @@ -18,16 +18,18 @@ package fr.acinq.eclair.db.sqlite import java.sql.{Connection, Statement} import java.util.UUID + import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.MilliSatoshi -import fr.acinq.eclair.channel.{AvailableBalanceChanged, Channel, ChannelErrorOccured, NetworkFeePaid} -import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats} -import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.channel.{AvailableBalanceChanged, Channel, ChannelErrorOccurred, NetworkFeePaid} +import fr.acinq.eclair.db._ +import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.ChannelCodecs import grizzled.slf4j.Logging + import scala.collection.immutable.Queue import scala.compat.Platform -import concurrent.duration._ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { @@ -37,7 +39,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { val DB_NAME = "audit" val CURRENT_VERSION = 3 - using(sqlite.createStatement()) { statement => + using(sqlite.createStatement(), inTransaction = true) { statement => def migration12(statement: Statement) = { statement.executeUpdate(s"ALTER TABLE sent ADD id BLOB DEFAULT '${ChannelCodecs.UNKNOWN_UUID.toString}' NOT NULL") @@ -74,7 +76,6 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)") statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)") statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)") - case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } } @@ -83,9 +84,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { using(sqlite.prepareStatement("INSERT INTO balance_updated VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.commitments.remoteParams.nodeId.value.toArray) - statement.setLong(3, e.localBalanceMsat) + statement.setLong(3, e.localBalance.toLong) statement.setLong(4, e.commitments.commitInput.txOut.amount.toLong) - statement.setLong(5, e.commitments.remoteParams.channelReserveSatoshis) // remote decides what our reserve should be + statement.setLong(5, e.commitments.remoteParams.channelReserve.toLong) // remote decides what our reserve should be statement.setLong(6, Platform.currentTime) statement.executeUpdate() } @@ -94,7 +95,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { using(sqlite.prepareStatement("INSERT INTO channel_events VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.remoteNodeId.value.toArray) - statement.setLong(3, e.capacitySat) + statement.setLong(3, e.capacity.toLong) statement.setBoolean(4, e.isFunder) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event) @@ -104,24 +105,29 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { override def add(e: PaymentSent): Unit = using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setLong(1, e.amount.toLong) - statement.setLong(2, e.feesPaid.toLong) - statement.setBytes(3, e.paymentHash.toArray) - statement.setBytes(4, e.paymentPreimage.toArray) - statement.setBytes(5, e.toChannelId.toArray) - statement.setLong(6, e.timestamp) - statement.setBytes(7, e.id.toString.getBytes) - - statement.executeUpdate() + e.parts.foreach(p => { + statement.setLong(1, p.amount.toLong) + statement.setLong(2, p.feesPaid.toLong) + statement.setBytes(3, e.paymentHash.toArray) + statement.setBytes(4, e.paymentPreimage.toArray) + statement.setBytes(5, p.toChannelId.toArray) + statement.setLong(6, p.timestamp) + statement.setBytes(7, p.id.toString.getBytes) + statement.addBatch() + }) + statement.executeBatch() } override def add(e: PaymentReceived): Unit = using(sqlite.prepareStatement("INSERT INTO received VALUES (?, ?, ?, ?)")) { statement => - statement.setLong(1, e.amount.toLong) - statement.setBytes(2, e.paymentHash.toArray) - statement.setBytes(3, e.fromChannelId.toArray) - statement.setLong(4, e.timestamp) - statement.executeUpdate() + e.parts.foreach(p => { + statement.setLong(1, p.amount.toLong) + statement.setBytes(2, e.paymentHash.toArray) + statement.setBytes(3, p.fromChannelId.toArray) + statement.setLong(4, p.timestamp) + statement.addBatch() + }) + statement.executeBatch() } override def add(e: PaymentRelayed): Unit = @@ -146,7 +152,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate() } - override def add(e: ChannelErrorOccured): Unit = + override def add(e: ChannelErrorOccurred): Unit = using(sqlite.prepareStatement("INSERT INTO channel_errors VALUES (?, ?, ?, ?, ?, ?)")) { statement => val (errorName, errorMessage) = e.error match { case Channel.LocalError(t) => (t.getClass.getSimpleName, t.getMessage) @@ -162,44 +168,49 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } override def listSent(from: Long, to: Long): Seq[PaymentSent] = - using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from.seconds.toMillis) - statement.setLong(2, to.seconds.toMillis) + using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() var q: Queue[PaymentSent] = Queue() while (rs.next()) { q = q :+ PaymentSent( - id = UUID.fromString(rs.getString("id")), - amount = MilliSatoshi(rs.getLong("amount_msat")), - feesPaid = MilliSatoshi(rs.getLong("fees_msat")), - paymentHash = rs.getByteVector32("payment_hash"), - paymentPreimage = rs.getByteVector32("payment_preimage"), - toChannelId = rs.getByteVector32("to_channel_id"), - timestamp = rs.getLong("timestamp")) + UUID.fromString(rs.getString("id")), + rs.getByteVector32("payment_hash"), + rs.getByteVector32("payment_preimage"), + Seq(PaymentSent.PartialPayment( + UUID.fromString(rs.getString("id")), + MilliSatoshi(rs.getLong("amount_msat")), + MilliSatoshi(rs.getLong("fees_msat")), + rs.getByteVector32("to_channel_id"), + None, // we don't store the route + rs.getLong("timestamp")))) } q } override def listReceived(from: Long, to: Long): Seq[PaymentReceived] = - using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from.seconds.toMillis) - statement.setLong(2, to.seconds.toMillis) + using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() var q: Queue[PaymentReceived] = Queue() while (rs.next()) { q = q :+ PaymentReceived( - amount = MilliSatoshi(rs.getLong("amount_msat")), - paymentHash = rs.getByteVector32("payment_hash"), - fromChannelId = rs.getByteVector32("from_channel_id"), - timestamp = rs.getLong("timestamp")) + rs.getByteVector32("payment_hash"), + Seq(PaymentReceived.PartialPayment( + MilliSatoshi(rs.getLong("amount_msat")), + rs.getByteVector32("from_channel_id"), + rs.getLong("timestamp") + ))) } q } override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] = - using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from.seconds.toMillis) - statement.setLong(2, to.seconds.toMillis) + using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() var q: Queue[PaymentRelayed] = Queue() while (rs.next()) { @@ -215,9 +226,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] = - using(sqlite.prepareStatement("SELECT * FROM network_fees WHERE timestamp >= ? AND timestamp < ?")) { statement => - statement.setLong(1, from.seconds.toMillis) - statement.setLong(2, to.seconds.toMillis) + using(sqlite.prepareStatement("SELECT * FROM network_fees WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() var q: Queue[NetworkFee] = Queue() while (rs.next()) { @@ -225,7 +236,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { remoteNodeId = PublicKey(rs.getByteVector("node_id")), channelId = rs.getByteVector32("channel_id"), txId = rs.getByteVector32("tx_id"), - feeSat = rs.getLong("fee_sat"), + fee = Satoshi(rs.getLong("fee_sat")), txType = rs.getString("tx_type"), timestamp = rs.getLong("timestamp")) } @@ -267,10 +278,10 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { while (rs.next()) { q = q :+ Stats( channelId = rs.getByteVector32("channel_id"), - avgPaymentAmountSatoshi = rs.getLong("avg_payment_amount_sat"), + avgPaymentAmount = Satoshi(rs.getLong("avg_payment_amount_sat")), paymentCount = rs.getInt("payment_count"), - relayFeeSatoshi = rs.getLong("relay_fee_sat"), - networkFeeSatoshi = rs.getLong("network_fee_sat")) + relayFee = Satoshi(rs.getLong("relay_fee_sat")), + networkFee = Satoshi(rs.getLong("network_fee_sat"))) } q } 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 d747a3b9dd..1641a73790 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.db.sqlite import java.sql.{Connection, Statement} import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.CltvExpiry import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec @@ -34,29 +35,36 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { val DB_NAME = "channels" val CURRENT_VERSION = 2 - private def migration12(statement: Statement) = { - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + // The SQLite documentation states that "It is not possible to enable or disable foreign key constraints in the middle + // of a multi-statement transaction (when SQLite is not in autocommit mode).". + // So we need to set foreign keys before we initialize tables / migrations (which is done inside a transaction). + using(sqlite.createStatement()) { statement => + statement.execute("PRAGMA foreign_keys = ON") } - using(sqlite.createStatement()) { statement => + using(sqlite.createStatement(), inTransaction = true) { statement => + + def migration12(statement: Statement) = { + statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + } + getVersion(statement, DB_NAME, CURRENT_VERSION) match { case 1 => logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION") migration12(statement) setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => - statement.execute("PRAGMA foreign_keys = ON") statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") - case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } + } override def addOrUpdateChannel(state: HasCommitments): Unit = { val data = stateDataCodec.encode(state).require.toByteArray - using (sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => + using(sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => update.setBytes(1, data) update.setBytes(2, state.channelId.toArray) if (update.executeUpdate() == 0) { @@ -93,24 +101,24 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { } } - def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: Long): Unit = { + def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = { using(sqlite.prepareStatement("INSERT OR IGNORE INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement => statement.setBytes(1, channelId.toArray) statement.setLong(2, commitmentNumber) statement.setBytes(3, paymentHash.toArray) - statement.setLong(4, cltvExpiry) + statement.setLong(4, cltvExpiry.toLong) statement.executeUpdate() } } - def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, Long)] = { + def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = { using(sqlite.prepareStatement("SELECT payment_hash, cltv_expiry FROM htlc_infos WHERE channel_id=? AND commitment_number=?")) { statement => statement.setBytes(1, channelId.toArray) statement.setLong(2, commitmentNumber) val rs = statement.executeQuery - var q: Queue[(ByteVector32, Long)] = Queue() + var q: Queue[(ByteVector32, CltvExpiry)] = Queue() while (rs.next()) { - q = q :+ (ByteVector32(rs.getByteVector32("payment_hash")), rs.getLong("cltv_expiry")) + q = q :+ (ByteVector32(rs.getByteVector32("payment_hash")), CltvExpiry(rs.getLong("cltv_expiry"))) } q } 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 0e60067498..4bfa7462c5 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 @@ -18,31 +18,83 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.db.NetworkDb -import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.router.PublicChannel import fr.acinq.eclair.wire.LightningMessageCodecs.nodeAnnouncementCodec import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} +import grizzled.slf4j.Logging +import scodec.Codec import scodec.bits.ByteVector -import scala.collection.immutable.Queue +import scala.collection.immutable.SortedMap -class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { +class SqliteNetworkDb(sqlite: Connection, chainHash: ByteVector32) extends NetworkDb with Logging { import SqliteUtils._ + import SqliteUtils.ExtendedResultSet._ val DB_NAME = "network" - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 + + import fr.acinq.eclair.wire.CommonCodecs._ + import scodec.codecs._ + + // on Android we prune as many fields as possible to save memory + val channelAnnouncementWitnessCodec = + ("features" | provide(null.asInstanceOf[ByteVector])) :: + ("chainHash" | provide(null.asInstanceOf[ByteVector32])) :: + ("shortChannelId" | shortchannelid) :: + ("nodeId1" | publicKey) :: + ("nodeId2" | publicKey) :: + ("bitcoinKey1" | provide(null.asInstanceOf[PublicKey])) :: + ("bitcoinKey2" | provide(null.asInstanceOf[PublicKey])) :: + ("unknownFields" | bytes) + + val channelAnnouncementCodec: Codec[ChannelAnnouncement] = ( + ("nodeSignature1" | provide(null.asInstanceOf[ByteVector64])) :: + ("nodeSignature2" | provide(null.asInstanceOf[ByteVector64])) :: + ("bitcoinSignature1" | provide(null.asInstanceOf[ByteVector64])) :: + ("bitcoinSignature2" | provide(null.asInstanceOf[ByteVector64])) :: + channelAnnouncementWitnessCodec).as[ChannelAnnouncement] + + val channelUpdateWitnessCodec = + ("chainHash" | provide(chainHash)) :: + ("shortChannelId" | shortchannelid) :: + ("timestamp" | uint32) :: + (("messageFlags" | byte) >>:~ { messageFlags => + ("channelFlags" | byte) :: + ("cltvExpiryDelta" | cltvExpiryDelta) :: + ("htlcMinimumMsat" | millisatoshi) :: + ("feeBaseMsat" | millisatoshi32) :: + ("feeProportionalMillionths" | uint32) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) :: + ("unknownFields" | bytes) + }) - using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION, s"incompatible version of $DB_NAME DB found") // there is only one version currently deployed - statement.execute("PRAGMA foreign_keys = ON") + val channelUpdateCodec: Codec[ChannelUpdate] = ( + ("signature" | provide(null.asInstanceOf[ByteVector64])) :: + channelUpdateWitnessCodec).as[ChannelUpdate] + + using(sqlite.createStatement(), inTransaction = true) { statement => + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case 1 => + // channel_update are cheap to retrieve, so let's just wipe them out and they'll get resynced + // on Android we also wipe the channel db + statement.execute("PRAGMA foreign_keys = ON") + logger.warn("migrating network db version 1->2") + statement.executeUpdate("DROP TABLE channels") + statement.executeUpdate("DROP TABLE channel_updates") + statement.execute("PRAGMA foreign_keys = OFF") + setVersion(statement, DB_NAME, CURRENT_VERSION) + logger.warn("migration complete") + case 2 => () // nothing to do + case unknown => throw new IllegalArgumentException(s"unknown version $unknown for network db") + } statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, node_id_1 BLOB NOT NULL, node_id_2 BLOB NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, timestamp INTEGER NOT NULL, flags BLOB NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, htlc_maximum_msat INTEGER, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid TEXT NOT NULL, channel_announcement BLOB NOT NULL, capacity_sat INTEGER NOT NULL, channel_update_1 BLOB NULL, channel_update_2 BLOB NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)") } @@ -85,108 +137,53 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb { } override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?, NULL, NULL)")) { statement => statement.setLong(1, c.shortChannelId.toLong) - statement.setBytes(2, c.nodeId1.value.toArray) // those will be 33-bytes compressed key - statement.setBytes(3, c.nodeId2.value.toArray) + statement.setString(2, txid.toHex) + statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray) + statement.setLong(4, capacity.toLong) statement.executeUpdate() } } - override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { - - def removeChannelsInternal(shortChannelIds: Iterable[ShortChannelId]): Unit = { - val ids = shortChannelIds.map(_.toLong).mkString(",") - using(sqlite.createStatement) { statement => - statement.execute("BEGIN TRANSACTION") - statement.executeUpdate(s"DELETE FROM channel_updates WHERE short_channel_id IN ($ids)") - statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id IN ($ids)") - statement.execute("COMMIT TRANSACTION") - } + override def updateChannel(u: ChannelUpdate): Unit = { + val column = if (u.isNode1) "channel_update_1" else "channel_update_2" + using(sqlite.prepareStatement(s"UPDATE channels SET $column=? WHERE short_channel_id=?")) { statement => + statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) + statement.setLong(2, u.shortChannelId.toLong) + statement.executeUpdate() } - - // remove channels by batch of 1000 - shortChannelIds.grouped(1000).foreach(removeChannelsInternal) } - override def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = { + override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT * FROM channels") - var m: Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = Map() - val emptyTxid = ByteVector32.Zeroes - val zeroCapacity = Satoshi(0) + val rs = statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") + var m = SortedMap.empty[ShortChannelId, PublicChannel] while (rs.next()) { - m = m + (ChannelAnnouncement( - nodeSignature1 = null, - nodeSignature2 = null, - bitcoinSignature1 = null, - bitcoinSignature2 = null, - features = null, - chainHash = null, - shortChannelId = ShortChannelId(rs.getLong("short_channel_id")), - nodeId1 = PublicKey.fromBin(ByteVector.view(rs.getBytes("node_id_1")), checkValid = false), // this will read a compressed or uncompressed serialized key (for backward compatibility reasons) to a compressed public key - nodeId2 = PublicKey.fromBin(ByteVector.view(rs.getBytes("node_id_2")), checkValid = false), - bitcoinKey1 = null, - bitcoinKey2 = null) -> (emptyTxid, zeroCapacity)) + val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value + val txId = ByteVector32.fromValidHex(rs.getString("txid")) + val capacity = rs.getLong("capacity_sat") + 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) + m = m + (ann.shortChannelId -> PublicChannel(ann, txId, Satoshi(capacity), channel_update_1_opt, channel_update_2_opt)) } m } } - override def addChannelUpdate(u: ChannelUpdate): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO channel_updates VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setLong(1, u.shortChannelId.toLong) - statement.setBoolean(2, Announcements.isNode1(u.channelFlags)) - statement.setLong(3, u.timestamp) - statement.setBytes(4, Array(u.messageFlags, u.channelFlags)) - statement.setInt(5, u.cltvExpiryDelta) - statement.setLong(6, u.htlcMinimumMsat) - statement.setLong(7, u.feeBaseMsat) - statement.setLong(8, u.feeProportionalMillionths) - setNullableLong(statement, 9, u.htlcMaximumMsat) - statement.executeUpdate() - } - } - - override def updateChannelUpdate(u: ChannelUpdate): Unit = { - using(sqlite.prepareStatement("UPDATE channel_updates SET timestamp=?, flags=?, cltv_expiry_delta=?, htlc_minimum_msat=?, fee_base_msat=?, fee_proportional_millionths=?, htlc_maximum_msat=? WHERE short_channel_id=? AND node_flag=?")) { statement => - statement.setLong(1, u.timestamp) - statement.setBytes(2, Array(u.messageFlags, u.channelFlags)) - statement.setInt(3, u.cltvExpiryDelta) - statement.setLong(4, u.htlcMinimumMsat) - statement.setLong(5, u.feeBaseMsat) - statement.setLong(6, u.feeProportionalMillionths) - setNullableLong(statement, 7, u.htlcMaximumMsat) - statement.setLong(8, u.shortChannelId.toLong) - statement.setBoolean(9, Announcements.isNode1(u.channelFlags)) - statement.executeUpdate() - } - } - - override def listChannelUpdates(): Seq[ChannelUpdate] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT * FROM channel_updates") - var q: Queue[ChannelUpdate] = Queue() - while (rs.next()) { - q = q :+ ChannelUpdate( - signature = null, - chainHash = null, - shortChannelId = ShortChannelId(rs.getLong("short_channel_id")), - timestamp = rs.getLong("timestamp"), - messageFlags = rs.getBytes("flags")(0), - channelFlags = rs.getBytes("flags")(1), - cltvExpiryDelta = rs.getInt("cltv_expiry_delta"), - htlcMinimumMsat = rs.getLong("htlc_minimum_msat"), - feeBaseMsat = rs.getLong("fee_base_msat"), - feeProportionalMillionths = rs.getLong("fee_proportional_millionths"), - htlcMaximumMsat = getNullableLong(rs, "htlc_maximum_msat")) + override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { + using(sqlite.createStatement) { statement => + shortChannelIds + .grouped(1000) // remove channels by batch of 1000 + .foreach {group => + val ids = shortChannelIds.map(_.toLong).mkString(",") + statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id IN ($ids)") } - q } } override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), disableAutoCommit = true) { statement => + using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), inTransaction = true) { statement => shortChannelIds.foreach(shortChannelId => { statement.setLong(1, shortChannelId.toLong) statement.addBatch() 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 8fd6ded566..0cdb65c15a 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,213 +16,303 @@ package fr.acinq.eclair.db.sqlite -import java.sql.Connection +import java.sql.{Connection, ResultSet, Statement} import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.db.{IncomingPayment, OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.wire.CommonCodecs import grizzled.slf4j.Logging +import scodec.Attempt +import scodec.codecs._ + import scala.collection.immutable.Queue -import OutgoingPaymentStatus._ -import concurrent.duration._ import scala.compat.Platform +import scala.concurrent.duration._ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { import SqliteUtils.ExtendedResultSet._ val DB_NAME = "payments" - val CURRENT_VERSION = 2 - - using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) <= CURRENT_VERSION, s"incompatible version of $DB_NAME DB found") // version 2 is "backward compatible" in the sense that it uses separate tables from version 1. There is no migration though - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)") - setVersion(statement, DB_NAME, CURRENT_VERSION) + val CURRENT_VERSION = 3 + + private val hopSummaryCodec = (("node_id" | CommonCodecs.publicKey) :: ("next_node_id" | CommonCodecs.publicKey) :: ("short_channel_id" | optional(bool, CommonCodecs.shortchannelid))).as[HopSummary] + private val paymentRouteCodec = discriminated[List[HopSummary]].by(byte) + .typecase(0x01, listOfN(uint8, hopSummaryCodec)) + private val failureSummaryCodec = (("type" | enumerated(uint8, FailureType)) :: ("message" | ascii32) :: paymentRouteCodec).as[FailureSummary] + private val paymentFailuresCodec = discriminated[List[FailureSummary]].by(byte) + .typecase(0x01, listOfN(uint8, failureSummaryCodec)) + + using(sqlite.createStatement(), inTransaction = true) { statement => + + def migration12(statement: Statement) = { + // Version 2 is "backwards compatible" in the sense that it uses separate tables from version 1 (which used a single "payments" table). + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)") + } + + def migration23(statement: Statement) = { + // We add many more columns to the sent_payments table. + statement.executeUpdate("DROP index payment_hash_idx") + statement.executeUpdate("ALTER TABLE sent_payments RENAME TO _sent_payments_old") + statement.executeUpdate("CREATE TABLE sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, target_node_id BLOB NOT NULL, created_at INTEGER NOT NULL, payment_request TEXT, completed_at INTEGER, payment_preimage BLOB, fees_msat INTEGER, payment_route BLOB, failures BLOB)") + // Old rows will be missing a target node id, so we use an easy-to-spot default value. + val defaultTargetNodeId = PrivateKey(ByteVector32.One).publicKey + statement.executeUpdate(s"INSERT INTO sent_payments (id, parent_id, payment_hash, amount_msat, target_node_id, created_at, completed_at, payment_preimage) SELECT id, id, payment_hash, amount_msat, X'${defaultTargetNodeId.toString}', created_at, completed_at, preimage FROM _sent_payments_old") + 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. + // 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("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") + + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_parent_id_idx ON sent_payments(parent_id)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_payment_hash_idx ON sent_payments(payment_hash)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_created_idx ON sent_payments(created_at)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_created_idx ON received_payments(created_at)") + } + + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case 1 => + logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION") + migration12(statement) + migration23(statement) + setVersion(statement, DB_NAME, CURRENT_VERSION) + case 2 => + logger.warn(s"migrating db $DB_NAME, found version=2 current=$CURRENT_VERSION") + migration23(statement) + setVersion(statement, DB_NAME, CURRENT_VERSION) + case CURRENT_VERSION => + statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, target_node_id BLOB NOT NULL, created_at INTEGER NOT NULL, payment_request TEXT, completed_at INTEGER, payment_preimage BLOB, fees_msat INTEGER, payment_route BLOB, failures BLOB)") + + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_parent_id_idx ON sent_payments(parent_id)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_payment_hash_idx ON sent_payments(payment_hash)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_created_idx ON sent_payments(created_at)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_created_idx ON received_payments(created_at)") + case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") + } + } override def addOutgoingPayment(sent: OutgoingPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) VALUES (?, ?, ?, ?, ?)")) { statement => + require(sent.status == OutgoingPaymentStatus.Pending, s"outgoing payment isn't pending (${sent.status.getClass.getSimpleName})") + using(sqlite.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => statement.setString(1, sent.id.toString) - statement.setBytes(2, sent.paymentHash.toArray) - statement.setLong(3, sent.amountMsat) - statement.setLong(4, sent.createdAt) - statement.setString(5, sent.status.toString) - val res = statement.executeUpdate() - logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") + statement.setString(2, sent.parentId.toString) + statement.setString(3, sent.externalId.orNull) + statement.setBytes(4, sent.paymentHash.toArray) + statement.setLong(5, sent.amount.toLong) + statement.setBytes(6, sent.targetNodeId.value.toArray) + statement.setLong(7, sent.createdAt) + statement.setString(8, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.executeUpdate() } } - override def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value, preimage: Option[ByteVector32] = None) = { - require((newStatus == SUCCEEDED && preimage.isDefined) || (newStatus == FAILED && preimage.isEmpty), "Wrong combination of state/preimage") + override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = + using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => + paymentResult.parts.foreach(p => { + statement.setLong(1, p.timestamp) + statement.setBytes(2, paymentResult.paymentPreimage.toArray) + statement.setLong(3, p.feesPaid.toLong) + statement.setBytes(4, paymentRouteCodec.encode(p.route.getOrElse(Nil).map(h => HopSummary(h)).toList).require.toByteArray) + statement.setString(5, p.id.toString) + statement.addBatch() + }) + if (statement.executeBatch().contains(0)) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as succeeded but already in final status (id=${paymentResult.id})") + } + + override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = + using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => + statement.setLong(1, paymentResult.timestamp) + statement.setBytes(2, paymentFailuresCodec.encode(paymentResult.failures.map(f => FailureSummary(f)).toList).require.toByteArray) + statement.setString(3, paymentResult.id.toString) + if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})") + } - using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, preimage, status) = (?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => - statement.setLong(1, Platform.currentTime) - statement.setBytes(2, if (preimage.isEmpty) null else preimage.get.toArray) - statement.setString(3, newStatus.toString) - statement.setString(4, id.toString) - if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to update an outgoing payment (id=$id) already in final status with=$newStatus") + private def parseOutgoingPayment(rs: ResultSet): OutgoingPayment = { + val result = OutgoingPayment( + UUID.fromString(rs.getString("id")), + UUID.fromString(rs.getString("parent_id")), + rs.getStringNullable("external_id"), + rs.getByteVector32("payment_hash"), + MilliSatoshi(rs.getLong("amount_msat")), + PublicKey(rs.getByteVector("target_node_id")), + rs.getLong("created_at"), + rs.getStringNullable("payment_request").map(PaymentRequest.read), + OutgoingPaymentStatus.Pending + ) + // If we have a pre-image, the payment succeeded. + rs.getByteVector32Nullable("payment_preimage") match { + case Some(paymentPreimage) => result.copy(status = OutgoingPaymentStatus.Succeeded( + paymentPreimage, + MilliSatoshi(rs.getLong("fees_msat")), + rs.getBitVectorOpt("payment_route").map(b => paymentRouteCodec.decode(b) match { + case Attempt.Successful(route) => route.value + case Attempt.Failure(_) => Nil + }).getOrElse(Nil), + rs.getLong("completed_at") + )) + case None => getNullableLong(rs, "completed_at") match { + // Otherwise if the payment was marked completed, it's a failure. + case Some(completedAt) => result.copy(status = OutgoingPaymentStatus.Failed( + rs.getBitVectorOpt("failures").map(b => paymentFailuresCodec.decode(b) match { + case Attempt.Successful(failures) => failures.value + case Attempt.Failure(_) => Nil + }).getOrElse(Nil), + completedAt + )) + // Else it's still pending. + case _ => result + } } } - override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE id = ?")) { statement => + override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = + using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE id = ?")) { statement => statement.setString(1, id.toString) val rs = statement.executeQuery() if (rs.next()) { - Some(OutgoingPayment( - UUID.fromString(rs.getString("id")), - rs.getByteVector32("payment_hash"), - rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), - rs.getLong("created_at"), - getNullableLong(rs, "completed_at"), - OutgoingPaymentStatus.withName(rs.getString("status")) - )) + Some(parseOutgoingPayment(rs)) } else { None } } - } - override def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => - statement.setBytes(1, paymentHash.toArray) + override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] = + using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE parent_id = ? ORDER BY created_at")) { statement => + statement.setString(1, parentId.toString) val rs = statement.executeQuery() var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { - q = q :+ OutgoingPayment( - UUID.fromString(rs.getString("id")), - rs.getByteVector32("payment_hash"), - rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), - rs.getLong("created_at"), - getNullableLong(rs, "completed_at"), - OutgoingPaymentStatus.withName(rs.getString("status")) - ) + q = q :+ parseOutgoingPayment(rs) } q } - } - override def listOutgoingPayments(): Seq[OutgoingPayment] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments") + override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = + using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE payment_hash = ? ORDER BY created_at")) { statement => + statement.setBytes(1, paymentHash.toArray) + val rs = statement.executeQuery() var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { - q = q :+ OutgoingPayment( - UUID.fromString(rs.getString("id")), - rs.getByteVector32("payment_hash"), - rs.getByteVector32Nullable("preimage"), - rs.getLong("amount_msat"), - rs.getLong("created_at"), - getNullableLong(rs, "completed_at"), - OutgoingPaymentStatus.withName(rs.getString("status")) - ) + q = q :+ parseOutgoingPayment(rs) } q } - } - override def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32): Unit = { - val insertStmt = pr.expiry match { - case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)" - case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at) VALUES (?, ?, ?, ?)" + override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] = + using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE created_at >= ? AND created_at < ? ORDER BY created_at")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) + val rs = statement.executeQuery() + var q: Queue[OutgoingPayment] = Queue() + while (rs.next()) { + q = q :+ parseOutgoingPayment(rs) + } + q } - using(sqlite.prepareStatement(insertStmt)) { statement => + override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32): Unit = + using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, pr.paymentHash.toArray) statement.setBytes(2, preimage.toArray) statement.setString(3, PaymentRequest.write(pr)) statement.setLong(4, pr.timestamp.seconds.toMillis) // BOLT11 timestamp is in seconds - pr.expiry.foreach { ex => statement.setLong(5, pr.timestamp.seconds.toMillis + ex.seconds.toMillis) } // we store "when" the invoice will expire, in milliseconds + statement.setLong(5, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis) statement.executeUpdate() } - } - override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ?")) { statement => - statement.setBytes(1, paymentHash.toArray) - val rs = statement.executeQuery() - if (rs.next()) { - Some(PaymentRequest.read(rs.getString("payment_request"))) - } else { - None - } + override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit = + using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => + statement.setLong(1, amount.toLong) + statement.setLong(2, receivedAt) + statement.setBytes(3, paymentHash.toArray) + val res = statement.executeUpdate() + if (res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") + } + + private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { + val paymentRequest = PaymentRequest.read(rs.getString("payment_request")) + val paymentPreimage = rs.getByteVector32("payment_preimage") + val createdAt = rs.getLong("created_at") + val received = getNullableLong(rs, "received_msat").map(MilliSatoshi(_)) + received match { + case Some(amount) => IncomingPayment(paymentRequest, paymentPreimage, createdAt, IncomingPaymentStatus.Received(amount, rs.getLong("received_at"))) + case None if paymentRequest.isExpired => IncomingPayment(paymentRequest, paymentPreimage, createdAt, IncomingPaymentStatus.Expired) + case None => IncomingPayment(paymentRequest, paymentPreimage, createdAt, IncomingPaymentStatus.Pending) } } - override def getPendingPaymentRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { - using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_hash = ? AND received_at IS NULL")) { statement => + override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = + using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - val preimage = rs.getByteVector32("preimage") - val pr = PaymentRequest.read(rs.getString("payment_request")) - Some(preimage, pr) + Some(parseIncomingPayment(rs)) } else { None } } - } - - override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = listPaymentRequests(from, to, pendingOnly = false) - - override def listPendingPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = listPaymentRequests(from, to, pendingOnly = true) - - def listPaymentRequests(from: Long, to: Long, pendingOnly: Boolean): Seq[PaymentRequest] = { - val queryStmt = pendingOnly match { - case true => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL ORDER BY created_at DESC" - case false => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at DESC" - } - - using(sqlite.prepareStatement(queryStmt)) { statement => - statement.setLong(1, from.seconds.toMillis) - statement.setLong(2, to.seconds.toMillis) - if (pendingOnly) statement.setLong(3, Platform.currentTime) + override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = + using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() - var q: Queue[PaymentRequest] = Queue() + var q: Queue[IncomingPayment] = Queue() while (rs.next()) { - q = q :+ PaymentRequest.read(rs.getString("payment_request")) + q = q :+ parseIncomingPayment(rs) } q } - } - override def addIncomingPayment(payment: IncomingPayment): Unit = { - using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => - statement.setLong(1, payment.amountMsat) - statement.setLong(2, payment.receivedAt) - statement.setBytes(3, payment.paymentHash.toArray) - val res = statement.executeUpdate() - if (res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") + override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = + using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) + val rs = statement.executeQuery() + var q: Queue[IncomingPayment] = Queue() + while (rs.next()) { + q = q :+ parseIncomingPayment(rs) + } + q } - } - override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { - using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => - statement.setBytes(1, paymentHash.toArray) + override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = + using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at > ? ORDER BY created_at")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) + statement.setLong(3, Platform.currentTime) val rs = statement.executeQuery() - if (rs.next()) { - Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) - } else { - None + var q: Queue[IncomingPayment] = Queue() + while (rs.next()) { + q = q :+ parseIncomingPayment(rs) } + q } - } - override def listIncomingPayments(): Seq[IncomingPayment] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") + override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = + using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) + statement.setLong(3, Platform.currentTime) + val rs = statement.executeQuery() var q: Queue[IncomingPayment] = Queue() while (rs.next()) { - q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) + q = q :+ parseIncomingPayment(rs) } q } - } } \ No newline at end of file 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 8d9e828ba3..11725d94fb 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 @@ -32,7 +32,7 @@ import SqliteUtils.ExtendedResultSet._ val DB_NAME = "peers" val CURRENT_VERSION = 1 - using(sqlite.createStatement()) { statement => + using(sqlite.createStatement(), inTransaction = true) { statement => require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION, s"incompatible version of $DB_NAME DB found") // there is only one version currently deployed statement.executeUpdate("CREATE TABLE IF NOT EXISTS peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala index b0621ac5e2..888e7388fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala @@ -33,7 +33,7 @@ class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb { val DB_NAME = "pending_relay" val CURRENT_VERSION = 1 - using(sqlite.createStatement()) { statement => + using(sqlite.createStatement(), inTransaction = true) { statement => require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION, s"incompatible version of $DB_NAME DB found") // there is only one version currently deployed // note: should we use a foreign key to local_channels table here? statement.executeUpdate("CREATE TABLE IF NOT EXISTS pending_relay (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 54aceb05bc..0e41038a4a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -27,31 +27,31 @@ import scala.collection.immutable.Queue object SqliteUtils { /** - * Manages closing of statement - * - * @param statement - * @param block - */ - def using[T <: Statement, U](statement: T, disableAutoCommit: Boolean = false)(block: T => U): U = { + * This helper makes sure statements are correctly closed. + * + * @param inTransaction if set to true, all updates in the block will be run in a transaction. + */ + def using[T <: Statement, U](statement: T, inTransaction: Boolean = false)(block: T => U): U = { try { - if (disableAutoCommit) statement.getConnection.setAutoCommit(false) - block(statement) + if (inTransaction) statement.getConnection.setAutoCommit(false) + val res = block(statement) + if (inTransaction) statement.getConnection.commit() + res + } catch { + case t: Exception => + if (inTransaction) statement.getConnection.rollback() + throw t } finally { - if (disableAutoCommit) statement.getConnection.setAutoCommit(true) + if (inTransaction) statement.getConnection.setAutoCommit(true) if (statement != null) statement.close() } } /** - * Several logical databases (channels, network, peers) may be stored in the same physical sqlite database. - * We keep track of their respective version using a dedicated table. The version entry will be created if - * there is none but will never be updated here (use setVersion to do that). - * - * @param statement - * @param db_name - * @param currentVersion - * @return - */ + * Several logical databases (channels, network, peers) may be stored in the same physical sqlite database. + * We keep track of their respective version using a dedicated table. The version entry will be created if + * there is none but will never be updated here (use setVersion to do that). + */ def getVersion(statement: Statement, db_name: String, currentVersion: Int): Int = { statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") // if there was no version for the current db, then insert the current version @@ -62,12 +62,8 @@ object SqliteUtils { } /** - * Updates the version for a particular logical database, it will overwrite the previous version. - * @param statement - * @param db_name - * @param newVersion - * @return - */ + * Updates the version for a particular logical database, it will overwrite the previous version. + */ def setVersion(statement: Statement, db_name: String, newVersion: Int) = { statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") // overwrite the existing version @@ -75,15 +71,10 @@ object SqliteUtils { } /** - * This helper assumes that there is a "data" column available, decodable with the provided codec - * - * TODO: we should use an scala.Iterator instead - * - * @param rs - * @param codec - * @tparam T - * @return - */ + * This helper assumes that there is a "data" column available, decodable with the provided codec + * + * TODO: we should use an scala.Iterator instead + */ def codecSequence[T](rs: ResultSet, codec: Codec[T]): Seq[T] = { var q: Queue[T] = Queue() while (rs.next()) { @@ -108,27 +99,22 @@ object SqliteUtils { } /** - * This helper retrieves the value from a nullable integer column and interprets it as an option. This is needed - * because `rs.getLong` would return `0` for a null value. - * It is used on Android only - * - * @param label - * @return - */ - def getNullableLong(rs: ResultSet, label: String) : Option[Long] = { + * This helper retrieves the value from a nullable integer column and interprets it as an option. This is needed + * because `rs.getLong` would return `0` for a null value. + * It is used on Android only + */ + def getNullableLong(rs: ResultSet, label: String): Option[Long] = { val result = rs.getLong(label) if (rs.wasNull()) None else Some(result) } /** - * Obtain an exclusive lock on a sqlite database. This is useful when we want to make sure that only one process - * accesses the database file (see https://www.sqlite.org/pragma.html). - * - * The lock will be kept until the database is closed, or if the locking mode is explicitly reset. - * - * @param sqlite - */ - def obtainExclusiveLock(sqlite: Connection){ + * Obtain an exclusive lock on a sqlite database. This is useful when we want to make sure that only one process + * accesses the database file (see https://www.sqlite.org/pragma.html). + * + * The lock will be kept until the database is closed, or if the locking mode is explicitly reset. + */ + def obtainExclusiveLock(sqlite: Connection) { val statement = sqlite.createStatement() statement.execute("PRAGMA locking_mode = EXCLUSIVE") // we have to make a write to actually obtain the lock @@ -138,17 +124,31 @@ object SqliteUtils { case class ExtendedResultSet(rs: ResultSet) { + def getBitVectorOpt(columnLabel: String): Option[BitVector] = Option(rs.getBytes(columnLabel)).map(BitVector(_)) + def getByteVector(columnLabel: String): ByteVector = ByteVector(rs.getBytes(columnLabel)) + def getByteVectorNullable(columnLabel: String): ByteVector = { + val result = rs.getBytes(columnLabel) + if (rs.wasNull()) ByteVector.empty else ByteVector(result) + } + def getByteVector32(columnLabel: String): ByteVector32 = ByteVector32(ByteVector(rs.getBytes(columnLabel))) def getByteVector32Nullable(columnLabel: String): Option[ByteVector32] = { val bytes = rs.getBytes(columnLabel) - if(rs.wasNull()) None else Some(ByteVector32(ByteVector(bytes))) + if (rs.wasNull()) None else Some(ByteVector32(ByteVector(bytes))) + } + + def getStringNullable(columnLabel: String): Option[String] = { + val result = rs.getString(columnLabel) + if (rs.wasNull()) None else Some(result) } + } object ExtendedResultSet { implicit def conv(rs: ResultSet): ExtendedResultSet = ExtendedResultSet(rs) } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala index f77b263547..2583672c2b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted import fr.acinq.eclair.io.Authenticator.{Authenticated, AuthenticationFailed, PendingAuth} import fr.acinq.eclair.wire.LightningMessageCodecs import fr.acinq.eclair.{Logs, NodeParams} +import kamon.Kamon /** * The purpose of this class is to serve as a buffer for newly connection before they are authenticated @@ -43,6 +44,7 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo def ready(switchboard: ActorRef, authenticating: Map[ActorRef, PendingAuth]): Receive = { case pending@PendingAuth(connection, remoteNodeId_opt, address, _) => log.debug(s"authenticating connection to ${address.getHostString}:${address.getPort} (pending=${authenticating.size} handlers=${context.children.size})") + Kamon.counter("peers.connecting.count").withTag("state", "authenticating").increment() val transport = context.actorOf(TransportHandler.props( KeyPair(nodeParams.nodeId.value, nodeParams.privateKey.value), remoteNodeId_opt.map(_.value), @@ -56,6 +58,7 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo import pendingAuth.{address, remoteNodeId_opt} val outgoing = remoteNodeId_opt.isDefined log.info(s"connection authenticated with $remoteNodeId@${address.getHostString}:${address.getPort} direction=${if (outgoing) "outgoing" else "incoming"}") + Kamon.counter("peers.connecting.count").withTag("state", "authenticated").increment() switchboard ! Authenticated(connection, transport, remoteNodeId, address, remoteNodeId_opt.isDefined, pendingAuth.origin_opt) context become ready(switchboard, authenticating - transport) 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 e9ddd74448..f1c466bb44 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 @@ -16,22 +16,21 @@ package fr.acinq.eclair.io -import java.io.ByteArrayInputStream import java.net.InetSocketAddress -import java.nio.ByteOrder import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated} import akka.event.Logging.MDC import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, MilliSatoshi, Protocol, Satoshi} +import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{secureRandom, wire, _} +import fr.acinq.eclair.{wire, _} +import kamon.Kamon import scodec.Attempt import scodec.bits.ByteVector @@ -40,8 +39,8 @@ import scala.concurrent.duration._ import scala.util.Random /** - * Created by PM on 26/08/2016. - */ + * Created by PM on 26/08/2016. + */ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { import Peer._ @@ -133,39 +132,36 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A when(INITIALIZING) { case Event(remoteInit: wire.Init, d: InitializingData) => d.transport ! TransportHandler.ReadAck(remoteInit) - val remoteHasInitialRoutingSync = Features.hasFeature(remoteInit.localFeatures, Features.INITIAL_ROUTING_SYNC_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) - val remoteHasChannelRangeQueriesExOptional = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) - val remoteHasChannelRangeQueriesExMandatory = Features.hasFeature(remoteInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) - val localHasChannelRangeQueriesOptional = Features.hasFeature(d.localInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) - val localHasChannelRangeQueriesMandatory = Features.hasFeature(d.localInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_BIT_MANDATORY) - val localHasChannelRangeQueriesExOptional = Features.hasFeature(d.localInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) - val localHasChannelRangeQueriesExMandatory = Features.hasFeature(d.localInit.localFeatures, Features.CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY) + log.info(s"peer is using globalFeatures=${remoteInit.globalFeatures.toBin} and localFeatures=${remoteInit.localFeatures.toBin}") - log.info(s"$remoteNodeId has features: initialRoutingSync=$remoteHasInitialRoutingSync channelRangeQueriesOptional=$remoteHasChannelRangeQueriesOptional channelRangeQueriesMandatory=$remoteHasChannelRangeQueriesMandatory") + if (Features.areSupported(remoteInit.localFeatures)) { d.origin_opt.foreach(origin => origin ! "connected") - if (remoteHasInitialRoutingSync) { - if (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory) { - // if they support extended channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support extended channel range queries, we do nothing (they will send us a query)", remoteNodeId) - } else if (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory) { - // if they support channel queries we do nothing, they will send us their filters - log.info("{} has set initial routing sync and support channel range queries, we do nothing (they will send us a query)", remoteNodeId) + import Features._ + + def hasLocalFeature(bit: Int) = Features.hasFeature(d.localInit.localFeatures, bit) + + def hasRemoteFeature(bit: Int) = Features.hasFeature(remoteInit.localFeatures, bit) + + val canUseChannelRangeQueries = (hasLocalFeature(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) || hasLocalFeature(CHANNEL_RANGE_QUERIES_BIT_MANDATORY)) && (hasRemoteFeature(CHANNEL_RANGE_QUERIES_BIT_OPTIONAL) || hasRemoteFeature(CHANNEL_RANGE_QUERIES_BIT_MANDATORY)) + + val canUseChannelRangeQueriesEx = (hasLocalFeature(CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) || hasLocalFeature(CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY)) && (hasRemoteFeature(CHANNEL_RANGE_QUERIES_EX_BIT_OPTIONAL) || hasRemoteFeature(CHANNEL_RANGE_QUERIES_EX_BIT_MANDATORY)) + + if (canUseChannelRangeQueries || canUseChannelRangeQueriesEx) { + // if they support channel queries we don't send routing info yet, if they want it they will query us + // we will query them, using extended queries if supported + val flags_opt = if (canUseChannelRangeQueriesEx) Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) else None + if (nodeParams.syncWhitelist.isEmpty || nodeParams.syncWhitelist.contains(remoteNodeId)) { + log.info(s"sending sync channel range query with flags_opt=$flags_opt") + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = flags_opt) } else { - // "old" nodes, do as before - router ! GetRoutingState + log.info("not syncing with this peer") } - } - // TODO: this is a hack for the Android version: don't send queries if we advertise that we don't support them - if ((localHasChannelRangeQueriesExOptional || localHasChannelRangeQueriesExMandatory) && (remoteHasChannelRangeQueriesExOptional || remoteHasChannelRangeQueriesExMandatory)) { - // if they support extended channel queries, always ask for their filter - router ! SendChannelQueryEx(remoteNodeId, d.transport) - } else if ((localHasChannelRangeQueriesOptional || localHasChannelRangeQueriesMandatory) && (remoteHasChannelRangeQueriesOptional || remoteHasChannelRangeQueriesMandatory)) { - // if they support channel queries, always ask for their filter - router ! SendChannelQuery(remoteNodeId, d.transport) + } else if (hasRemoteFeature(INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) { + // "old" nodes, do as before + log.info("peer requested a full routing table dump") + router ! GetRoutingState } // let's bring existing/requested channels online @@ -293,20 +289,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A stay case Event(c: Peer.OpenChannel, d: ConnectedData) => - val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender)) + val (channel, localParams) = createNewChannel(nodeParams, 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.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags)) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.transport, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(msg: wire.OpenChannel, d: ConnectedData) => d.transport ! TransportHandler.ReadAck(msg) d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingSatoshis = msg.fundingSatoshis, origin_opt = None) + val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) val temporaryChannelId = msg.temporaryChannelId log.info(s"accepting a new channel to $remoteNodeId temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.transport, d.remoteInit) @@ -339,7 +335,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A // we won't clean it up, but we won't remember the temporary id on channel termination stay using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) - case Event(RoutingState(channels, updates, nodes), d: ConnectedData) => + case Event(RoutingState(channels, nodes), d: ConnectedData) => // let's send the messages def send(announcements: Iterable[_ <: LightningMessage]) = announcements.foldLeft(0) { case (c, ann) => @@ -348,9 +344,9 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } log.info(s"sending all announcements to {}", remoteNodeId) - val channelsSent = send(channels) + val channelsSent = send(channels.map(_.ann)) val nodesSent = send(nodes) - val updatesSent = send(updates) + val updatesSent = send(channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq)) log.info(s"sent all announcements to {}: channels={} updates={} nodes={}", remoteNodeId, channelsSent, updatesSent, nodesSent) stay @@ -361,8 +357,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A case Event(DelayedRebroadcast(rebroadcast), d: ConnectedData) => /** - * Send and count in a single iteration - */ + * Send and count in a single iteration + */ def sendAndCount(msgs: Map[_ <: RoutingMessage, Set[ActorRef]]): Int = msgs.foldLeft(0) { case (count, (_, origins)) if origins.contains(self) => // the announcement came from this peer, we don't send it back @@ -523,22 +519,38 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A } /** - * The transition INSTANTIATING -> DISCONNECTED happens in 2 scenarios - * - Manual connection to a new peer: then when(DISCONNECTED) we expect a Peer.Connect from the switchboard - * - Eclair restart: The switchboard creates the peers and sends Init and then Peer.Reconnect to trigger reconnection attempts - * - * So when we see this transition we NO-OP because we don't want to start a Reconnect timer but the peer will receive the trigger - * (Connect/Reconnect) messages from the switchboard. - */ + * The transition INSTANTIATING -> DISCONNECTED happens in 2 scenarios + * - Manual connection to a new peer: then when(DISCONNECTED) we expect a Peer.Connect from the switchboard + * - Eclair restart: The switchboard creates the peers and sends Init and then Peer.Reconnect to trigger reconnection attempts + * + * So when we see this transition we NO-OP because we don't want to start a Reconnect timer but the peer will receive the trigger + * (Connect/Reconnect) messages from the switchboard. + */ onTransition { case INSTANTIATING -> DISCONNECTED => () case _ -> DISCONNECTED if nodeParams.autoReconnect => setTimer(RECONNECT_TIMER, Reconnect, Random.nextInt(nodeParams.initialRandomReconnectDelay.toMillis.toInt).millis, repeat = false) // we add some randomization to not have peers reconnect to each other exactly at the same time case DISCONNECTED -> _ if nodeParams.autoReconnect => cancelTimer(RECONNECT_TIMER) } - def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingSatoshis: Long, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { + onTransition { + case _ -> CONNECTED => + //Metrics.connectedPeers.increment() + context.system.eventStream.publish(PeerConnected(self, remoteNodeId)) + case CONNECTED -> DISCONNECTED => + //Metrics.connectedPeers.decrement() + context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId)) + } + + onTermination { + case StopEvent(_, CONNECTED, d: ConnectedData) => + // the transition handler won't be fired if we go directly from CONNECTED to closed + //Metrics.connectedPeers.decrement() + context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId)) + } + + def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) - val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis) + val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingAmount) val channel = spawnChannel(nodeParams, origin_opt) (channel, localParams) } @@ -618,10 +630,10 @@ object Peer { case class Disconnect(nodeId: PublicKey) case object ResumeAnnouncements case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[Long], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) { - require(fundingSatoshis.amount < Channel.MAX_FUNDING_SATOSHIS, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING_SATOSHIS}") - require(pushMsat.amount <= 1000 * fundingSatoshis.amount, s"pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis.amount >= 0, s"fundingSatoshis must be positive") - require(pushMsat.amount >= 0, s"pushMsat must be positive") + require(fundingSatoshis < Channel.MAX_FUNDING, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING}") + 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") fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum $MinimumFeeratePerKw rate/kw")) } case object GetPeerInfo @@ -641,22 +653,26 @@ object Peer { // @formatter:on - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long): LocalParams = { - val entropy = new Array[Byte](16) - secureRandom.nextBytes(entropy) - val bis = new ByteArrayInputStream(entropy) - val channelKeyPath = DeterministicWallet.KeyPath(Seq(Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN), Protocol.uint32(bis, ByteOrder.BIG_ENDIAN))) - makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingSatoshis, channelKeyPath) + object Metrics { +// val peers = Kamon.rangeSampler("peers.count").withoutTags() +// val connectedPeers = Kamon.rangeSampler("peers.connected.count").withoutTags() +// val channels = Kamon.rangeSampler("channels.count").withoutTags() + } + + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + // we make sure that funder and fundee key path end differently + val fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isFunder) + makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingAmount, fundingKeyPath) } - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingSatoshis: Long, channelKeyPath: DeterministicWallet.KeyPath): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( nodeParams.nodeId, - channelKeyPath, - dustLimitSatoshis = nodeParams.dustLimitSatoshis, + fundingKeyPath, + dustLimit = nodeParams.dustLimit, maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = Math.max((nodeParams.reserveToFundingRatio * fundingSatoshis).toLong, nodeParams.dustLimitSatoshis), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimumMsat = nodeParams.htlcMinimumMsat, + channelReserve = (fundingAmount * nodeParams.reserveToFundingRatio).max(nodeParams.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + htlcMinimum = nodeParams.htlcMinimum, toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, defaultFinalScriptPubKey = defaultFinalScriptPubKey, @@ -666,13 +682,13 @@ object Peer { } /** - * Peer may want to filter announcements based on timestamp - * - * @param gossipTimestampFilter_opt optional gossip timestamp range - * @return - * - true if there is a filter and msg has no timestamp, or has one that matches the filter - * - false otherwise - */ + * Peer may want to filter announcements based on timestamp + * + * @param gossipTimestampFilter_opt optional gossip timestamp range + * @return + * - true if there is a filter and msg has no timestamp, or has one that matches the filter + * - false otherwise + */ def timestampInRange(msg: RoutingMessage, gossipTimestampFilter_opt: Option[GossipTimestampFilter]): Boolean = { // check if this message has a timestamp that matches our timestamp filter (msg, gossipTimestampFilter_opt) match { @@ -685,7 +701,7 @@ object Peer { def hostAndPort2InetSocketAddress(hostAndPort: HostAndPort): InetSocketAddress = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort) /** - * Exponential backoff retry with a finite max - */ + * Exponential backoff retry with a finite max + */ def nextReconnectionDelay(currentDelay: FiniteDuration, maxReconnectInterval: FiniteDuration): FiniteDuration = (2 * currentDelay).min(maxReconnectInterval) } 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 new file mode 100644 index 0000000000..9d25a2b04a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala @@ -0,0 +1,26 @@ +/* + * 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.io + +import akka.actor.ActorRef +import fr.acinq.bitcoin.Crypto.PublicKey + +sealed trait PeerEvent + +case class PeerConnected(peer: ActorRef, nodeId: PublicKey) extends PeerEvent + +case class PeerDisconnected(peer: ActorRef, nodeId: PublicKey) extends PeerEvent 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 2c39b004fd..2d58a99d03 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 @@ -23,6 +23,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.eclair.NodeParams +import kamon.Kamon import scala.concurrent.Promise @@ -52,6 +53,7 @@ class Server(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke def listening(listener: ActorRef): Receive = { case Connected(remote, _) => log.info(s"connected to $remote") + Kamon.counter("peers.connecting.count").withTag("state", "connected").increment() val connection = sender authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = None, address = remote, origin_opt = None) 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 c0a17bd6e7..06f1ef48f3 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 @@ -31,6 +31,7 @@ import fr.acinq.eclair.payment.{Relayed, Relayer} import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc} import grizzled.slf4j.Logging +import scodec.bits.ByteVector import scala.util.Success @@ -56,7 +57,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto }) val peers = nodeParams.db.peers.listPeers() - checkBrokenHtlcsLink(channels, nodeParams.privateKey) match { + checkBrokenHtlcsLink(channels, nodeParams.privateKey, nodeParams.globalFeatures) match { case Nil => () case brokenHtlcs => val brokenHtlcKiller = context.system.actorOf(Props[HtlcReaper], name = "htlc-reaper") @@ -162,7 +163,7 @@ object Switchboard extends Logging { * * This check will detect this and will allow us to fast-fail HTLCs and thus preserve channels. */ - def checkBrokenHtlcsLink(channels: Seq[HasCommitments], privateKey: PrivateKey): Seq[UpdateAddHtlc] = { + def checkBrokenHtlcsLink(channels: Seq[HasCommitments], privateKey: PrivateKey, features: ByteVector): Seq[UpdateAddHtlc] = { // 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 @@ -171,7 +172,7 @@ object Switchboard extends Logging { .flatMap(_.commitments.remoteCommit.spec.htlcs) .filter(_.direction == OUT) .map(_.add) - .map(Relayer.decryptPacket(_, privateKey)) + .map(Relayer.decryptPacket(_, privateKey, features)) .collect { case Right(RelayPayload(add, _, _)) => add } // we only consider htlcs that are relayed, not the ones for which we are the final node // Here we do it differently because we need the origin information. 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 44adabf9eb..f14482fccf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -23,15 +23,14 @@ import fr.acinq.bitcoin._ import scodec.Attempt import scodec.bits.{BitVector, ByteVector} -import scala.concurrent.duration.Duration import scala.util.{Failure, Success, Try} package object eclair { /** - * We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux - * See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/ - */ + * We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux + * See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/ + */ val secureRandom = new SecureRandom() def randomBytes(length: Int): ByteVector = { @@ -59,82 +58,80 @@ package object eclair { } /** - * Converts feerate in satoshi-per-bytes to feerate in satoshi-per-kw - * - * @param feeratePerByte fee rate in satoshi-per-bytes - * @return feerate in satoshi-per-kw - */ + * Converts fee rate in satoshi-per-bytes to fee rate in satoshi-per-kw + * + * @param feeratePerByte fee rate in satoshi-per-bytes + * @return fee rate in satoshi-per-kw + */ def feerateByte2Kw(feeratePerByte: Long): Long = feerateKB2Kw(feeratePerByte * 1000) /** - * - * @param feeratesPerKw fee rate in satoshi-per-kw - * @return fee rate in satoshi-per-byte - */ - def feerateKw2Byte(feeratesPerKw: Long): Long = feeratesPerKw / 250 + * Converts fee rate in satoshi-per-kw to fee rate in satoshi-per-byte + * + * @param feeratePerKw fee rate in satoshi-per-kw + * @return fee rate in satoshi-per-byte + */ + def feerateKw2Byte(feeratePerKw: Long): Long = feeratePerKw / 250 /** - why 253 and not 250 since feerate-per-kw is feerate-per-kb / 250 and the minimum relay fee rate is 1000 satoshi/Kb ? - - because bitcoin core uses neither the actual tx size in bytes or the tx weight to check fees, but a "virtual size" - which is (3 * weight) / 4 ... - so we want : - fee > 1000 * virtual size - feerate-per-kw * weight > 1000 * (3 * weight / 4) - feerate_per-kw > 250 + 3000 / (4 * weight) - with a conservative minimum weight of 400, we get a minimum feerate_per-kw of 253 - - see https://github.com/ElementsProject/lightning/pull/1251 + * why 253 and not 250 since feerate-per-kw is feerate-per-kb / 250 and the minimum relay fee rate is 1000 satoshi/Kb ? + * + * because bitcoin core uses neither the actual tx size in bytes or the tx weight to check fees, but a "virtual size" + * which is (3 * weight) / 4 ... + * so we want : + * fee > 1000 * virtual size + * feerate-per-kw * weight > 1000 * (3 * weight / 4) + * feerate_per-kw > 250 + 3000 / (4 * weight) + * with a conservative minimum weight of 400, we get a minimum feerate_per-kw of 253 + * + * see https://github.com/ElementsProject/lightning/pull/1251 **/ val MinimumFeeratePerKw = 253 /** - minimum relay fee rate, in satoshi per kilo - bitcoin core uses virtual size and not the actual size in bytes, see above + * minimum relay fee rate, in satoshi per kilo + * bitcoin core uses virtual size and not the actual size in bytes, see above **/ val MinimumRelayFeeRate = 1000 /** - * Converts feerate in satoshi-per-kilobytes to feerate in satoshi-per-kw - * - * @param feeratePerKB fee rate in satoshi-per-kilobytes - * @return feerate in satoshi-per-kw - */ + * Converts fee rate in satoshi-per-kilobytes to fee rate in satoshi-per-kw + * + * @param feeratePerKB fee rate in satoshi-per-kilobytes + * @return fee rate in satoshi-per-kw + */ def feerateKB2Kw(feeratePerKB: Long): Long = Math.max(feeratePerKB / 4, MinimumFeeratePerKw) /** - * - * @param feeratesPerKw fee rate in satoshi-per-kw - * @return fee rate in satoshi-per-kilobyte - */ - def feerateKw2KB(feeratesPerKw: Long): Long = feeratesPerKw * 4 - + * Converts fee rate in satoshi-per-kw to fee rate in satoshi-per-kilobyte + * + * @param feeratePerKw fee rate in satoshi-per-kw + * @return fee rate in satoshi-per-kilobyte + */ + def feerateKw2KB(feeratePerKw: Long): Long = feeratePerKw * 4 def isPay2PubkeyHash(address: String): Boolean = address.startsWith("1") || address.startsWith("m") || address.startsWith("n") /** - * Tests whether the binary data is composed solely of printable ASCII characters (see BOLT 1) - * - * @param data to check - */ + * Tests whether the binary data is composed solely of printable ASCII characters (see BOLT 1) + * + * @param data to check + */ def isAsciiPrintable(data: ByteVector): Boolean = data.toArray.forall(ch => ch >= 32 && ch < 127) /** - * - * @param baseMsat fixed fee - * @param proportional proportional fee - * @param msat amount in millisatoshi - * @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis - */ - def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000 + * @param baseFee fixed fee + * @param proportionalFee proportional fee (millionths) + * @param paymentAmount payment amount in millisatoshi + * @return the fee that a node should be paid to forward an HTLC of 'paymentAmount' millisatoshis + */ + def nodeFee(baseFee: MilliSatoshi, proportionalFee: Long, paymentAmount: MilliSatoshi): MilliSatoshi = baseFee + (paymentAmount * proportionalFee) / 1000000 /** - * - * @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. - */ - + * @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 { case Success((Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)) if chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash => Script.pay2pkh(pubKeyHash) @@ -155,8 +152,37 @@ package object eclair { } } - /** - * We use this in the context of timestamp filtering, when we don't need an upper bound. - */ - val MaxEpochSeconds = Duration.fromNanos(Long.MaxValue).toSeconds + implicit class LongToBtcAmount(l: Long) { + // @formatter:off + def msat: MilliSatoshi = MilliSatoshi(l) + def sat: Satoshi = Satoshi(l) + def mbtc: MilliBtc = MilliBtc(l) + def btc: Btc = Btc(l) + // @formatter:on + } + + // We implement Numeric to take advantage of operations such as sum, sort or min/max on iterables. + implicit object NumericMilliSatoshi extends Numeric[MilliSatoshi] { + // @formatter:off + override def plus(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = x + y + override def minus(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = x - y + override def times(x: MilliSatoshi, y: MilliSatoshi): MilliSatoshi = MilliSatoshi(x.toLong * y.toLong) + override def negate(x: MilliSatoshi): MilliSatoshi = -x + override def fromInt(x: Int): MilliSatoshi = MilliSatoshi(x) + override def toInt(x: MilliSatoshi): Int = x.toLong.toInt + override def toLong(x: MilliSatoshi): Long = x.toLong + override def toFloat(x: MilliSatoshi): Float = x.toLong + override def toDouble(x: MilliSatoshi): Double = x.toLong + override def compare(x: MilliSatoshi, y: MilliSatoshi): Int = x.compare(y) + // @formatter:on + } + + implicit class ToMilliSatoshiConversion(amount: BtcAmount) { + // @formatter:off + def toMilliSatoshi: MilliSatoshi = MilliSatoshi.toMilliSatoshi(amount) + def +(other: MilliSatoshi): MilliSatoshi = amount.toMilliSatoshi + other + def -(other: MilliSatoshi): MilliSatoshi = amount.toMilliSatoshi - other + // @formatter:on + } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala index 6136b928e4..1036925221 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Auditor.scala @@ -20,9 +20,10 @@ import akka.actor.{Actor, ActorLogging, Props} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.NodeParams import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError} -import fr.acinq.eclair.channel.Helpers.Closing.{LocalClose, MutualClose, RecoveryClose, RemoteClose, RevokedClose} +import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent} +import kamon.Kamon import scala.concurrent.ExecutionContext import scala.concurrent.duration._ @@ -34,7 +35,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[PaymentEvent]) context.system.eventStream.subscribe(self, classOf[NetworkFeePaid]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) - context.system.eventStream.subscribe(self, classOf[ChannelErrorOccured]) + context.system.eventStream.subscribe(self, classOf[ChannelErrorOccurred]) context.system.eventStream.subscribe(self, classOf[ChannelStateChanged]) context.system.eventStream.subscribe(self, classOf[ChannelClosed]) @@ -42,26 +43,61 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { override def receive: Receive = { - case e: PaymentSent => db.add(e) - - case e: PaymentReceived => db.add(e) - - case e: PaymentRelayed => db.add(e) + case e: PaymentSent => + Kamon + .histogram("payment.hist") + .withTag("direction", "sent") + .record(e.amount.truncateToSatoshi.toLong) + db.add(e) + + case e: PaymentReceived => + Kamon + .histogram("payment.hist") + .withTag("direction", "received") + .record(e.amount.truncateToSatoshi.toLong) + db.add(e) + + case e: PaymentRelayed => + Kamon + .histogram("payment.hist") + .withTag("direction", "relayed") + .withTag("type", "total") + .record(e.amountIn.truncateToSatoshi.toLong) + Kamon + .histogram("payment.hist") + .withTag("direction", "relayed") + .withTag("type", "fee") + .record((e.amountIn - e.amountOut).truncateToSatoshi.toLong) + db.add(e) case e: NetworkFeePaid => db.add(e) case e: AvailableBalanceChanged => balanceEventThrottler ! e - case e: ChannelErrorOccured => db.add(e) + case e: ChannelErrorOccurred => + val metric = Kamon.counter("channels.errors") + e.error match { + case LocalError(_) if e.isFatal => metric.withTag("origin", "local").withTag("fatal", "yes").increment() + case LocalError(_) if !e.isFatal => metric.withTag("origin", "local").withTag("fatal", "no").increment() + case RemoteError(_) => metric.withTag("origin", "remote").increment() + } + db.add(e) case e: ChannelStateChanged => + val metric = Kamon.counter("channels.lifecycle") + // NB: order matters! e match { case ChannelStateChanged(_, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, d: DATA_NORMAL) => - db.add(ChannelLifecycleEvent(d.channelId, remoteNodeId, d.commitments.commitInput.txOut.amount.toLong, d.commitments.localParams.isFunder, !d.commitments.announceChannel, "created")) + metric.withTag("event", "created").increment() + db.add(ChannelLifecycleEvent(d.channelId, remoteNodeId, d.commitments.commitInput.txOut.amount, d.commitments.localParams.isFunder, !d.commitments.announceChannel, "created")) + case ChannelStateChanged(_, _, _, WAIT_FOR_INIT_INTERNAL, _, _) => + case ChannelStateChanged(_, _, _, _, CLOSING, _) => + metric.withTag("event", "closing").increment() case _ => () } case e: ChannelClosed => + Kamon.counter("channels.lifecycle").withTag("event", "closed").increment() val event = e.closingType match { case MutualClose => "mutual" case LocalClose => "local" @@ -69,7 +105,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { case RecoveryClose => "recovery" case RevokedClose => "revoked" } - db.add(ChannelLifecycleEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount.toLong, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) + db.add(ChannelLifecycleEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) } @@ -77,8 +113,8 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging { } /** - * We don't want to log every tiny payment, and we don't want to log probing events. - */ + * We don't want to log every tiny payment, and we don't want to log probing events. + */ class BalanceEventThrottler(db: AuditDb) extends Actor with ActorLogging { import ExecutionContext.Implicits.global @@ -99,21 +135,21 @@ class BalanceEventThrottler(db: AuditDb) extends Actor with ActorLogging { // we delay the processing of the event in order to smooth variations log.info(s"will log balance event in $delay for channelId=${e.channelId}") context.system.scheduler.scheduleOnce(delay, self, ProcessEvent(e.channelId)) - context.become(run(pending + (e.channelId -> (BalanceUpdate(e, e))))) + context.become(run(pending + (e.channelId -> BalanceUpdate(e, e)))) case Some(BalanceUpdate(first, _)) => // we already are about to log a balance event, let's update the data we have log.info(s"updating balance data for channelId=${e.channelId}") - context.become(run(pending + (e.channelId -> (BalanceUpdate(first, e))))) + context.become(run(pending + (e.channelId -> BalanceUpdate(first, e)))) } case ProcessEvent(channelId) => pending.get(channelId) match { case Some(BalanceUpdate(first, last)) => - if (first.commitments.remoteCommit.spec.toRemoteMsat == last.localBalanceMsat) { + if (first.commitments.remoteCommit.spec.toRemote == last.localBalance) { // we don't log anything if the balance didn't change (e.g. it was a probe payment) log.info(s"ignoring balance event for channelId=$channelId (changed was discarded)") } else { - log.info(s"processing balance event for channelId=$channelId balance=${first.localBalanceMsat}->${last.localBalanceMsat}") + log.info(s"processing balance event for channelId=$channelId balance=${first.localBalance}->${last.localBalance}") // we log the last event, which contains the most up to date balance db.add(last) context.become(run(pending - channelId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 94e33deb94..fc738ac934 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -19,17 +19,17 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket -import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment} -import fr.acinq.eclair.router.{Announcements, Data} -import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails} -import fr.acinq.eclair.{NodeParams, randomBytes32, secureRandom} +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest +import fr.acinq.eclair.router.{Announcements, Data, PublicChannel} +import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails +import fr.acinq.eclair.{LongToBtcAmount, NodeParams, randomBytes32, secureRandom} import scala.concurrent.duration._ /** - * This actor periodically probes the network by sending payments to random nodes. The payments will eventually fail - * because the recipient doesn't know the preimage, but it allows us to test channels and improve routing for real payments. - */ + * This actor periodically probes the network by sending payments to random nodes. The payments will eventually fail + * because the recipient doesn't know the preimage, but it allows us to test channels and improve routing for real payments. + */ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) extends Actor with ActorLogging { import Autoprobe._ @@ -54,15 +54,15 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case Some(targetNodeId) => val paymentHash = randomBytes32 // we don't even know the preimage (this needs to be a secure random!) log.info(s"sending payment probe to node=$targetNodeId payment_hash=$paymentHash") - paymentInitiator ! SendPayment(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1) + paymentInitiator ! SendPaymentRequest(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1) case None => log.info(s"could not find a destination, re-scheduling") scheduleProbe() } - case paymentResult: PaymentResult => + case paymentResult: PaymentEvent => paymentResult match { - case PaymentFailed(_, _, _ :+ RemoteFailure(_, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_)))) => + case PaymentFailed(_, _, _ :+ RemoteFailure(_, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_, _))), _) => log.info(s"payment probe successful to node=$targetNodeId") case _ => log.info(s"payment probe failed with paymentResult=$paymentResult") @@ -83,15 +83,16 @@ object Autoprobe { val PROBING_INTERVAL = 20 seconds - val PAYMENT_AMOUNT_MSAT = 100 * 1000 // this is below dust_limit so there won't be an output in the commitment tx + val PAYMENT_AMOUNT_MSAT = (100 * 1000) msat // this is below dust_limit so there won't be an output in the commitment tx object TickProbe def pickPaymentDestination(nodeId: PublicKey, routingData: Data): Option[PublicKey] = { // we only pick direct peers with enabled public channels - val peers = routingData.updates + val peers = routingData.channels .collect { - case (desc, u) if desc.a == nodeId && Announcements.isEnabled(u.channelFlags) && routingData.channels.contains(u.shortChannelId) => desc.b // we only consider outgoing channels that are enabled and announced + case (shortChannelId, c@PublicChannel(ann, _, _, Some(u1), _)) + if c.getNodeIdSameSideAs(u1) == nodeId && Announcements.isEnabled(u1.channelFlags) && routingData.channels.exists(_._1 == shortChannelId) => ann.nodeId2 // we only consider outgoing channels that are enabled and announced } if (peers.isEmpty) { None diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 1070f3ef37..d5d399f932 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -17,25 +17,23 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, Props, Status} -import fr.acinq.bitcoin.{Crypto, MilliSatoshi} +import fr.acinq.bitcoin.Crypto import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} -import fr.acinq.eclair.db.IncomingPayment +import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus} import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, NodeParams, randomBytes32} -import concurrent.duration._ -import scala.compat.Platform +import fr.acinq.eclair.{NodeParams, randomBytes32} + import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} /** - * Simple payment handler that generates payment requests and fulfills incoming htlcs. - * - * Note that unfulfilled payment requests are kept forever if they don't have an expiry! - * - * Created by PM on 17/06/2016. - */ + * Simple payment handler that generates payment requests and fulfills incoming htlcs. + * + * Note that unfulfilled payment requests are kept forever if they don't have an expiry! + * + * Created by PM on 17/06/2016. + */ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging { implicit val ec: ExecutionContext = context.system.dispatcher @@ -50,7 +48,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) - paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) + paymentDb.addIncomingPayment(paymentRequest, paymentPreimage) paymentRequest } match { case Success(paymentRequest) => sender ! paymentRequest @@ -58,32 +56,35 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } case htlc: UpdateAddHtlc => - paymentDb.getPendingPaymentRequestAndPreimage(htlc.paymentHash) match { - case Some((paymentPreimage, paymentRequest)) => - val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) + paymentDb.getIncomingPayment(htlc.paymentHash) match { + case Some(IncomingPayment(_, _, _, status)) if status.isInstanceOf[IncomingPaymentStatus.Received] => + log.warning(s"ignoring incoming payment for paymentHash=${htlc.paymentHash} which has already been paid") + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) + case Some(IncomingPayment(paymentRequest, paymentPreimage, _, _)) => + val minFinalExpiry = paymentRequest.minFinalCltvExpiryDelta.getOrElse(Channel.MIN_CLTV_EXPIRY_DELTA).toCltvExpiry(nodeParams.currentBlockHeight) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however // it must not be greater than two times the requested amount. // see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages paymentRequest.amount match { case _ if paymentRequest.isExpired => - sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) case _ if htlc.cltvExpiry < minFinalExpiry => - sender ! CMD_FAIL_HTLC(htlc.id, Right(FinalExpiryTooSoon), commit = true) - case Some(amount) if MilliSatoshi(htlc.amountMsat) < amount => - log.warning(s"received payment with amount too small for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") - sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) - case Some(amount) if MilliSatoshi(htlc.amountMsat) > amount * 2 => - log.warning(s"received payment with amount too large for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") - sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) + case Some(amount) if htlc.amountMsat < amount => + log.warning(s"received payment with amount too small for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) + case Some(amount) if htlc.amountMsat > amount * 2 => + log.warning(s"received payment with amount too large for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) case _ => - log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") + log.info(s"received payment for paymentHash=${htlc.paymentHash} amount=${htlc.amountMsat}") // amount is correct or was not specified in the payment request - nodeParams.db.payments.addIncomingPayment(IncomingPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime)) + nodeParams.db.payments.receiveIncomingPayment(htlc.paymentHash, htlc.amountMsat) sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true) - context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId)) + context.system.eventStream.publish(PaymentReceived(htlc.paymentHash, PaymentReceived.PartialPayment(htlc.amountMsat, htlc.channelId) :: Nil)) } case None => - sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat)), commit = true) + sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(htlc.amountMsat, nodeParams.currentBlockHeight)), commit = true) } } 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 5901dce810..8dafe453d7 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 @@ -17,20 +17,102 @@ package fr.acinq.eclair.payment import java.util.UUID -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.router.Hop + import scala.compat.Platform /** - * Created by PM on 01/02/2017. - */ + * Created by PM on 01/02/2017. + */ + sealed trait PaymentEvent { val paymentHash: ByteVector32 + val timestamp: Long +} + +case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent { + require(parts.nonEmpty, "must have at least one subpayment") + val amount: MilliSatoshi = parts.map(_.amount).sum + val feesPaid: MilliSatoshi = parts.map(_.feesPaid).sum + val timestamp: Long = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled } -case class PaymentSent(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +object PaymentSent { + + case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: Long = Platform.currentTime) { + require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop") + } + +} + +case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: Long = Platform.currentTime) extends PaymentEvent case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent -case class PaymentReceived(amount: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent { + require(parts.nonEmpty, "must have at least one subpayment") + val amount: MilliSatoshi = parts.map(_.amount).sum + val timestamp: Long = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts +} + +object PaymentReceived { + + case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime) + +} case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent + +sealed trait PaymentFailure + +/** A failure happened locally, preventing the payment from being sent (e.g. no route found). */ +case class LocalFailure(t: Throwable) extends PaymentFailure + +/** A remote node failed the payment and we were able to decrypt the onion failure packet. */ +case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure + +/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */ +case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure + +object PaymentFailure { + + import fr.acinq.bitcoin.Crypto.PublicKey + import fr.acinq.eclair.channel.AddHtlcFailed + import fr.acinq.eclair.router.RouteNotFound + import fr.acinq.eclair.wire.Update + + /** + * Rewrites a list of failures to retrieve the meaningful part. + * + * If a list of failures with many elements ends up with a LocalFailure RouteNotFound, this RouteNotFound failure + * should be removed. This last failure is irrelevant information. In such a case only the n-1 attempts were rejected + * with a **significant reason**; the final RouteNotFound error provides no meaningful insight. + * + * This method should be used by the user interface to provide a non-exhaustive but more useful feedback. + * + * @param failures a list of payment failures for a payment + */ + def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { + failures.map { + case LocalFailure(AddHtlcFailed(_, _, t, _, _, _)) => LocalFailure(t) // we're interested in the error which caused the add-htlc to fail + case other => other + } match { + case previousFailures :+ LocalFailure(RouteNotFound) if previousFailures.nonEmpty => previousFailures + case other => other + } + } + + /** + * This allows us to detect if a bad node always answers with a new update (e.g. with a slightly different expiry or fee) + * in order to mess with us. + */ + def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean = + failures + .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update } + .isDefined + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index de9bef237a..863ba579ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -17,25 +17,51 @@ package fr.acinq.eclair.payment import java.util.UUID + import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import fr.acinq.eclair.NodeParams -import fr.acinq.eclair.payment.PaymentLifecycle.GenericSendPayment +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.payment.PaymentLifecycle.{DefaultPaymentProgressHandler, SendPayment, SendPaymentToRoute} +import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.router.RouteParams +import fr.acinq.eclair.wire.Onion.FinalLegacyPayload +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, NodeParams} /** - * Created by PM on 29/08/2016. - */ + * Created by PM on 29/08/2016. + */ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with ActorLogging { override def receive: Receive = { - case c: GenericSendPayment => + case p: PaymentInitiator.SendPaymentRequest => val paymentId = UUID.randomUUID() - val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register)) - payFsm forward c + // We add one block in order to not have our htlc fail when a new block has just been found. + val finalExpiry = p.finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1) + val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, DefaultPaymentProgressHandler(paymentId, p, nodeParams.db.payments), router, register)) + // NB: we only generate legacy payment onions for now for maximum compatibility. + p.predefinedRoute match { + case Nil => payFsm forward SendPayment(p.paymentHash, p.targetNodeId, FinalLegacyPayload(p.amount, finalExpiry), p.maxAttempts, p.assistedRoutes, p.routeParams) + case hops => payFsm forward SendPaymentToRoute(p.paymentHash, hops, FinalLegacyPayload(p.amount, finalExpiry)) + } sender ! paymentId } } object PaymentInitiator { + def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], nodeParams, router, register) + + case class SendPaymentRequest(amount: MilliSatoshi, + paymentHash: ByteVector32, + targetNodeId: PublicKey, + maxAttempts: Int, + finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, + paymentRequest: Option[PaymentRequest] = None, + externalId: Option[String] = None, + predefinedRoute: Seq[PublicKey] = Nil, + assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + routeParams: Option[RouteParams] = None) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index e56d2f36de..7f9810bcd2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -18,44 +18,47 @@ package fr.acinq.eclair.payment import java.util.UUID -import akka.actor.{ActorRef, FSM, Props, Status} +import akka.actor.{ActorContext, ActorRef, FSM, Props, Status} +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.WatchEventSpentBasic -import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.{BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT, CMD_ADD_HTLC, Register} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} -import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.router._ +import fr.acinq.eclair.wire.Onion._ import fr.acinq.eclair.wire._ import scodec.Attempt import scodec.bits.ByteVector -import concurrent.duration._ import scala.compat.Platform import scala.util.{Failure, Success} /** - * Created by PM on 26/08/2016. - */ -class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { + * Created by PM on 26/08/2016. + */ + +class PaymentLifecycle(nodeParams: NodeParams, progressHandler: PaymentProgressHandler, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { - val paymentsDb = nodeParams.db.payments + val id = progressHandler.id startWith(WAITING_FOR_REQUEST, WaitingForRequest) when(WAITING_FOR_REQUEST) { case Event(c: SendPaymentToRoute, WaitingForRequest) => - val send = SendPayment(c.amountMsat, c.paymentHash, c.hops.last, finalCltvExpiry = c.finalCltvExpiry, maxAttempts = 1) - paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) + val send = SendPayment(c.paymentHash, c.hops.last, c.finalPayload, maxAttempts = 1) router ! FinalizeRoute(c.hops) + progressHandler.onSend() goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil) case Event(c: SendPayment, WaitingForRequest) => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) - paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Platform.currentTime, None, OutgoingPaymentStatus.PENDING)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams) + progressHandler.onSend() goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -63,26 +66,21 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(RouteResponse(hops, ignoreNodes, ignoreChannels), WaitingForRoute(s, c, failures)) => log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}") val firstHop = hops.head - // we add one block in order to not have our htlc fail when a new block has just been found - val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + 1 - - val (cmd, sharedSecrets) = buildCommand(id, c.amountMsat, finalExpiry, c.paymentHash, hops) + val (cmd, sharedSecrets) = buildCommand(id, c.paymentHash, hops, c.finalPayload) register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops) case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => - reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) + progressHandler.onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t)))(context) stop(FSM.Normal) } when(WAITING_FOR_PAYMENT_COMPLETE) { - case Event("ok", _) => stay() + case Event("ok", _) => stay - case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, preimage = Some(fulfill.paymentPreimage)) - reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) - context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, route)) => + val p = PartialPayment(id, c.finalPayload.amount, cmd.amount - c.finalPayload.amount, fulfill.channelId, Some(route)) + progressHandler.onSuccess(s, PaymentSent(id, c.paymentHash, fulfill.paymentPreimage, p :: Nil))(context) stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => @@ -90,8 +88,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") - reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) + progressHandler.onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ RemoteFailure(hops, e)))(context) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -104,20 +101,19 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis UnreadableRemoteFailure(hops) } log.warning(s"too many failed attempts, failing the payment") - reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) + progressHandler.onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ failure))(context) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") // in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") @@ -132,7 +128,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis log.info(s"received exact same update from nodeId=$nodeId, excluding the channel from futures routes") val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId)) - case Some(u) if hasAlreadyFailedOnce(nodeId, failures) => + case Some(u) if PaymentFailure.hasAlreadyFailedOnce(nodeId, failures) => // this node had already given us a new channel update and is still unhappy, it is probably messing with us, let's exclude it log.warning(s"it is the second time nodeId=$nodeId answers with a new update, excluding it: old=$u new=${failureMessage.update}") val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId @@ -145,11 +141,11 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // in any case, we forward the update to the router router ! failureMessage.update // let's try again, router will have updated its state - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) } else { // this node is fishy, it gave us a bad sig!! let's filter it out log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}") - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => @@ -162,7 +158,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis router ! WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(hop.lastUpdate.shortChannelId)) ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) } - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) } @@ -182,60 +178,77 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // note that if the channel is in fact still alive, we will get it again via network announcements anyway log.warning(s"local shortChannelId=${fwd.shortChannelId} doesn't seem to exist, excluding it from routes") router ! WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(fwd.shortChannelId)) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures) case _ => if (failures.size + 1 >= c.maxAttempts) { - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) - reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) + progressHandler.onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t)))(context) stop(FSM.Normal) } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") val faultyChannel = ChannelDesc(hops.head.lastUpdate.shortChannelId, hops.head.nodeId, hops.head.nextNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t)) } } + } whenUnhandled { case Event(_: TransportHandler.ReadAck, _) => stay // ignored, router replies with this when we forward a channel_update } - def reply(to: ActorRef, e: PaymentResult) = { - to ! e - context.system.eventStream.publish(e) - } - initialize() } object PaymentLifecycle { - def props(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], nodeParams, id, router, register) + def props(nodeParams: NodeParams, progressHandler: PaymentProgressHandler, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], nodeParams, progressHandler, router, register) + + /** This handler notifies other components of payment progress. */ + trait PaymentProgressHandler { + val id: UUID + + // @formatter:off + def onSend(): Unit + def onSuccess(sender: ActorRef, result: PaymentSent)(ctx: ActorContext): Unit + def onFailure(sender: ActorRef, result: PaymentFailed)(ctx: ActorContext): Unit + // @formatter:on + } + + /** Normal payments are stored in the payments DB and emit payment events. */ + case class DefaultPaymentProgressHandler(id: UUID, r: SendPaymentRequest, db: PaymentsDb) extends PaymentProgressHandler { + + override def onSend(): Unit = { + db.addOutgoingPayment(OutgoingPayment(id, id, r.externalId, r.paymentHash, r.amount, r.targetNodeId, Platform.currentTime, r.paymentRequest, OutgoingPaymentStatus.Pending)) + } + + override def onSuccess(sender: ActorRef, result: PaymentSent)(ctx: ActorContext): Unit = { + db.updateOutgoingPayment(result) + sender ! result + ctx.system.eventStream.publish(result) + } + + override def onFailure(sender: ActorRef, result: PaymentFailed)(ctx: ActorContext): Unit = { + db.updateOutgoingPayment(result) + sender ! result + ctx.system.eventStream.publish(result) + } + + } // @formatter:off - case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, paymentPreimage: Option[ByteVector32] = None) - sealed trait GenericSendPayment - case class SendPaymentToRoute(amountMsat: Long, paymentHash: ByteVector32, hops: Seq[PublicKey], finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY) extends GenericSendPayment - case class SendPayment(amountMsat: Long, - paymentHash: ByteVector32, + case class ReceivePayment(amount_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None, paymentPreimage: Option[ByteVector32] = None) + case class SendPaymentToRoute(paymentHash: ByteVector32, hops: Seq[PublicKey], finalPayload: FinalPayload) + case class SendPayment(paymentHash: ByteVector32, targetNodeId: PublicKey, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, - finalCltvExpiry: Long = Channel.MIN_CLTV_EXPIRY, + finalPayload: FinalPayload, maxAttempts: Int, - routeParams: Option[RouteParams] = None) extends GenericSendPayment { - require(amountMsat > 0, s"amountMsat must be > 0") + assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + routeParams: Option[RouteParams] = None) { + require(finalPayload.amount > 0.msat, s"amount must be > 0") } - sealed trait PaymentResult - case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees - sealed trait PaymentFailure - case class LocalFailure(t: Throwable) extends PaymentFailure - case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure - case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure - case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult - sealed trait Data case object WaitingForRequest extends Data case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure]) extends Data @@ -245,89 +258,57 @@ object PaymentLifecycle { case object WAITING_FOR_REQUEST extends State case object WAITING_FOR_ROUTE extends State case object WAITING_FOR_PAYMENT_COMPLETE extends State - // @formatter:on - def buildOnion(nodes: Seq[PublicKey], payloads: Seq[PerHopPayload], associatedData: ByteVector32): Sphinx.PacketAndSecrets = { require(nodes.size == payloads.size) val sessionKey = randomKey - val payloadsbin: Seq[ByteVector] = payloads - .map(OnionCodecs.perHopPayloadCodec.encode) + val payloadsBin: Seq[ByteVector] = payloads + .map { + case p: FinalPayload => OnionCodecs.finalPerHopPayloadCodec.encode(p) + case p: RelayPayload => OnionCodecs.relayPerHopPayloadCodec.encode(p) + } .map { case Attempt.Successful(bitVector) => bitVector.toByteVector case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.PaymentPacket.create(sessionKey, nodes, payloadsbin, associatedData) + Sphinx.PaymentPacket.create(sessionKey, nodes, payloadsBin, associatedData) } /** - * - * @param finalAmountMsat the final htlc amount in millisatoshis - * @param finalExpiry the final htlc expiry in number of blocks - * @param hops the hops as computed by the router + extra routes from payment request - * @return a (firstAmountMsat, firstExpiry, payloads) tuple where: - * - firstAmountMsat is the amount for the first htlc in the route - * - firstExpiry is the cltv expiry for the first htlc in the route - * - a sequence of payloads that will be used to build the onion - */ - def buildPayloads(finalAmountMsat: Long, finalExpiry: Long, hops: Seq[Hop]): (Long, Long, Seq[PerHopPayload]) = - hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(ShortChannelId(0L), finalAmountMsat, finalExpiry) :: Nil)) { - case ((msat, expiry, payloads), hop) => - val nextFee = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, msat) - (msat + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads) + * Build the onion payloads for each hop. + * + * @param hops the hops as computed by the router + extra routes from payment request + * @param finalPayload payload data for the final node (amount, expiry, additional tlv records, etc) + * @return a (firstAmount, firstExpiry, payloads) tuple where: + * - firstAmount is the amount for the first htlc in the route + * - firstExpiry is the cltv expiry for the first htlc in the route + * - a sequence of payloads that will be used to build the onion + */ + def buildPayloads(hops: Seq[Hop], finalPayload: FinalPayload): (MilliSatoshi, CltvExpiry, Seq[PerHopPayload]) = { + hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PerHopPayload](finalPayload))) { + case ((amount, expiry, payloads), hop) => + val nextFee = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, amount) + // Since we don't have any scenario where we add tlv data for intermediate hops, we use legacy payloads. + val payload = RelayLegacyPayload(hop.lastUpdate.shortChannelId, amount, expiry) + (amount + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, payload +: payloads) } + } - def buildCommand(id: UUID, finalAmountMsat: Long, finalExpiry: Long, paymentHash: ByteVector32, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { - val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) + def buildCommand(id: UUID, paymentHash: ByteVector32, hops: Seq[Hop], finalPayload: FinalPayload): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { + val (firstAmount, firstExpiry, payloads) = buildPayloads(hops.drop(1), finalPayload) val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmount, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets } /** - * Rewrites a list of failures to retrieve the meaningful part. - *

- * If a list of failures with many elements ends up with a LocalFailure RouteNotFound, this RouteNotFound failure - * should be removed. This last failure is irrelevant information. In such a case only the n-1 attempts were rejected - * with a **significant reason** ; the final RouteNotFound error provides no meaningful insight. - *

- * This method should be used by the user interface to provide a non-exhaustive but more useful feedback. - * - * @param failures a list of payment failures for a payment - */ - def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { - failures.map { - case LocalFailure(AddHtlcFailed(_, _, t, _, _, _)) => LocalFailure(t) // we're interested in the error which caused the add-htlc to fail - case other => other - } match { - case previousFailures :+ LocalFailure(RouteNotFound) if previousFailures.nonEmpty => previousFailures - case other => other - } - } - - /** - * This method retrieves the channel update that we used when we built a route. - * - * It just iterates over the hops, but there are at most 20 of them. - * - * @param nodeId - * @param hops - * @return the channel update if found - */ + * This method retrieves the channel update that we used when we built a route. + * It just iterates over the hops, but there are at most 20 of them. + * + * @return the channel update if found + */ def getChannelUpdateForNode(nodeId: PublicKey, hops: Seq[Hop]): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) - /** - * This allows us to detect if a bad node always answers with a new update (e.g. with a slightly different expiry or fee) - * in order to mess with us. - * - * @param nodeId - * @param failures - * @return - */ - def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean = - failures - .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update } - .isDefined } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index b5c6da04f9..81ed66c7f6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -17,52 +17,50 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{MilliSatoshi, _} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.payment.PaymentRequest._ +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId} import scodec.Codec import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} -import scala.concurrent.duration._ + import scala.compat.Platform +import scala.concurrent.duration._ import scala.util.Try /** - * Lightning Payment Request - * see https://github.com/lightningnetwork/lightning-rfc/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 - * @param signature request signature that will be checked against node id - */ + * Lightning Payment Request + * see https://github.com/lightningnetwork/lightning-rfc/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 + * @param signature request signature that will be checked against node id + */ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) { - amount.map(a => require(a.amount > 0, 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") + 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") /** - * - * @return the payment hash - */ + * @return the payment hash + */ lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHash => p }.get.hash /** - * - * @return the description of the payment, or its hash - */ + * @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) }.get /** - * - * @return the fallback address if any. It could be a script address, pubkey address, .. - */ + * @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) } @@ -73,21 +71,22 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam case expiry: PaymentRequest.Expiry => expiry.toLong } - lazy val minFinalCltvExpiry: Option[Long] = tags.collectFirst { - case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toLong + lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst { + case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } + lazy val features: Features = tags.collectFirst { case f: Features => f }.getOrElse(Features(BitVector.empty)) + def isExpired: Boolean = expiry match { case Some(expiryTime) => timestamp + expiryTime <= Platform.currentTime.milliseconds.toSeconds case None => timestamp + DEFAULT_EXPIRY_SECONDS <= Platform.currentTime.milliseconds.toSeconds } /** - * - * @return the hash of this payment request - */ + * @return the hash of this payment request + */ def hash: ByteVector32 = { - val hrp = s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8") + 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 bin = Codecs.bolt11DataCodec.encode(data).require val message = ByteVector.view(hrp) ++ bin.dropRight(520).toByteVector @@ -95,10 +94,9 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam } /** - * - * @param priv private key - * @return a signed payment request - */ + * @param priv private key + * @return a signed payment request + */ def sign(priv: PrivateKey): PaymentRequest = { val sig64 = Crypto.sign(hash, priv) val (pub1, _) = Crypto.recoverPublicKey(sig64, hash) @@ -119,7 +117,8 @@ object PaymentRequest { def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey, description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, - extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = { + extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L, + features: Option[Features] = None): PaymentRequest = { val prefix = prefixes(chainHash) @@ -132,8 +131,9 @@ object PaymentRequest { Some(PaymentHash(paymentHash)), Some(Description(description)), fallbackAddress.map(FallbackAddress(_)), - expirySeconds.map(Expiry(_)) - ).flatten ++ extraHops.map(RoutingInfo(_)), + expirySeconds.map(Expiry(_)), + features + ).flatten ++ extraHops.map(RoutingInfo), signature = ByteVector.empty) .sign(privateKey) } @@ -149,7 +149,6 @@ object PaymentRequest { case class UnknownTag1(data: BitVector) extends UnknownTaggedField case class UnknownTag2(data: BitVector) extends UnknownTaggedField case class UnknownTag4(data: BitVector) extends UnknownTaggedField - case class UnknownTag5(data: BitVector) extends UnknownTaggedField case class UnknownTag7(data: BitVector) extends UnknownTaggedField case class UnknownTag8(data: BitVector) extends UnknownTaggedField case class UnknownTag10(data: BitVector) extends UnknownTaggedField @@ -174,39 +173,37 @@ object PaymentRequest { // @formatter:on /** - * Payment Hash - * - * @param hash payment hash - */ + * Payment Hash + * + * @param hash payment hash + */ case class PaymentHash(hash: ByteVector32) extends TaggedField /** - * Description - * - * @param description a free-format string that will be included in the payment request - */ + * Description + * + * @param description a free-format string that will be included in the payment request + */ 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, ... - */ + * 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, ... + */ case class DescriptionHash(hash: ByteVector32) extends TaggedField /** - * Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed - * - */ + * Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed + */ case class FallbackAddress(version: Byte, data: ByteVector) extends TaggedField object FallbackAddress { /** - * - * @param address valid base58 or bech32 address - * @return a FallbackAddressTag instance - */ + * @param address valid base58 or bech32 address + * @return a FallbackAddressTag instance + */ def apply(address: String): FallbackAddress = { Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get } @@ -241,10 +238,9 @@ object PaymentRequest { } /** - * This returns a bitvector with the minimum size necessary to encode the long, left padded - * to have a length (in bits) multiples of 5 - * @param l - */ + * This returns a bitvector with the minimum size necessary to encode the long, left padded + * to have a length (in bits) multiples of 5 + */ def long2bits(l: Long) = { val bin = BitVector.fromLong(l) var highest = -1 @@ -254,59 +250,69 @@ object PaymentRequest { val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest) nonPadded.size % 5 match { case 0 => nonPadded - case remaining => BitVector.fill(5 - remaining)(false) ++ nonPadded + case remaining => BitVector.fill(5 - remaining)(high = false) ++ nonPadded } } /** - * Extra hop contained in RoutingInfoTag - * - * @param nodeId start of the channel - * @param shortChannelId channel id - * @param feeBaseMsat node fixed fee - * @param feeProportionalMillionths node proportional fee - * @param cltvExpiryDelta node cltv expiry delta - */ - case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBaseMsat: Long, feeProportionalMillionths: Long, cltvExpiryDelta: Int) + * Extra hop contained in RoutingInfoTag + * + * @param nodeId start of the channel + * @param shortChannelId channel id + * @param feeBase node fixed fee + * @param feeProportionalMillionths node proportional fee + * @param cltvExpiryDelta node cltv expiry delta + */ + case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) /** - * Routing Info - * - * @param path one or more entries containing extra routing information for a private route - */ + * Routing Info + * + * @param path one or more entries containing extra routing information for a private route + */ case class RoutingInfo(path: List[ExtraHop]) extends TaggedField /** - * Expiry Date - */ + * Expiry Date + */ case class Expiry(bin: BitVector) extends TaggedField { def toLong: Long = bin.toLong(signed = false) } object Expiry { /** - * @param seconds expiry data for this payment request - */ + * @param seconds expiry data for this payment request + */ def apply(seconds: Long): Expiry = Expiry(long2bits(seconds)) } /** - * Min final CLTV expiry - * - */ + * Min final CLTV expiry + */ case class MinFinalCltvExpiry(bin: BitVector) extends TaggedField { - def toLong: Long = bin.toLong(signed = false) + def toCltvExpiryDelta = CltvExpiryDelta(bin.toInt(signed = false)) } object MinFinalCltvExpiry { /** - * Min final CLTV expiry - * - * @param blocks min final cltv expiry, in blocks - */ + * Min final CLTV expiry + * + * @param blocks min final cltv expiry, in blocks + */ def apply(blocks: Long): MinFinalCltvExpiry = MinFinalCltvExpiry(long2bits(blocks)) } + /** + * Features supported or required for receiving this payment. + */ + case class Features(bitmask: BitVector) extends TaggedField + + object Features { + def apply(features: Int*): Features = Features(long2bits(features.foldLeft(0) { + case (current, feature) => current + (1 << feature) + })) + } + object Codecs { import fr.acinq.eclair.wire.CommonCodecs._ @@ -317,9 +323,9 @@ object PaymentRequest { val extraHopCodec: Codec[ExtraHop] = ( ("nodeId" | publicKey) :: ("shortChannelId" | shortchannelid) :: - ("fee_base_msat" | uint32) :: + ("fee_base_msat" | millisatoshi32) :: ("fee_proportional_millionth" | uint32) :: - ("cltv_expiry_delta" | uint16) + ("cltv_expiry_delta" | cltvExpiryDelta) ).as[ExtraHop] val extraHopsLengthCodec = Codec[Int]( @@ -329,7 +335,7 @@ object PaymentRequest { def alignedBytesCodec[A](valueCodec: Codec[A]): Codec[A] = Codec[A]( (value: A) => valueCodec.encode(value), - (wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(false))).map(_._1).decode(wire) // the 'constant' codec ensures that padding is zero + (wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(high = false))).map(_._1).decode(wire) // the 'constant' codec ensures that padding is zero ) val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt) @@ -342,7 +348,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[UnknownTag5]) + .typecase(5, dataCodec(bits).as[Features]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) @@ -388,10 +394,9 @@ object PaymentRequest { object Amount { /** - * @param amount - * @return the unit allowing for the shortest representation possible - */ - def unit(amount: MilliSatoshi): Char = amount.amount * 10 match { // 1 milli-satoshis == 10 pico-bitcoin + * @return the unit allowing for the shortest representation possible + */ + def unit(amount: MilliSatoshi): Char = amount.toLong * 10 match { // 1 milli-satoshis == 10 pico-bitcoin case pico if pico % 1000 > 0 => 'p' case pico if pico % 1000000 > 0 => 'n' case pico if pico % 1000000000 > 0 => 'u' @@ -411,10 +416,10 @@ object PaymentRequest { def encode(amount: Option[MilliSatoshi]): String = { amount match { case None => "" - case Some(amt) if unit(amt) == 'p' => s"${amt.amount * 10L}p" // 1 pico-bitcoin == 10 milli-satoshis - case Some(amt) if unit(amt) == 'n' => s"${amt.amount / 100L}n" - case Some(amt) if unit(amt) == 'u' => s"${amt.amount / 100000L}u" - case Some(amt) if unit(amt) == 'm' => s"${amt.amount / 100000000L}m" + case Some(amt) if unit(amt) == 'p' => s"${amt.toLong * 10L}p" // 1 pico-bitcoin == 10 milli-satoshis + case Some(amt) if unit(amt) == 'n' => s"${amt.toLong / 100L}n" + case Some(amt) if unit(amt) == 'u' => s"${amt.toLong / 100000L}u" + case Some(amt) if unit(amt) == 'm' => s"${amt.toLong / 100000000L}m" } } } @@ -428,10 +433,9 @@ object PaymentRequest { val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5)) /** - * - * @param input bech32-encoded payment request - * @return a payment request - */ + * @param input bech32-encoded payment request + * @return a payment request + */ def read(input: String): PaymentRequest = { // used only for data validation Bech32.decode(input) @@ -439,7 +443,7 @@ object PaymentRequest { val separatorIndex = lowercaseInput.lastIndexOf('1') val hrp = lowercaseInput.take(separatorIndex) val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix")) - val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.size - 6)) // 6 == checksum size + val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value val signature = ByteVector64(bolt11Data.signature.take(64)) val message: ByteVector = ByteVector.view(hrp.getBytes) ++ data.dropRight(520).toByteVector // we drop the sig bytes @@ -460,10 +464,9 @@ object PaymentRequest { } /** - * - * @param pr payment request - * @return a bech32-encoded payment request - */ + * @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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index d5d6b9f17e..bb32da3e73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -20,25 +20,23 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.db.OutgoingPaymentStatus -import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{NodeParams, ShortChannelId, nodeFee} +import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, UInt64, nodeFee} import grizzled.slf4j.Logging +import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} import scala.collection.mutable // @formatter:off - sealed trait Origin case class Local(id: UUID, sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors -case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin +case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountIn: MilliSatoshi, amountOut: MilliSatoshi) extends Origin sealed trait ForwardMessage case class ForwardAdd(add: UpdateAddHtlc, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends ForwardMessage @@ -47,14 +45,12 @@ case class ForwardFail(fail: UpdateFailHtlc, to: Origin, htlc: UpdateAddHtlc) ex case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage case object GetUsableBalances -case class UsableBalances(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSendMsat: Long, canReceiveMsat: Long, isPublic: Boolean) - +case class UsableBalances(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean) // @formatter:on - /** - * Created by PM on 01/02/2017. - */ + * Created by PM on 01/02/2017. + */ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) extends Actor with ActorLogging { import Relayer._ @@ -77,8 +73,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR .map(o => UsableBalances( remoteNodeId = o.nextNodeId, shortChannelId = o.channelUpdate.shortChannelId, - canSendMsat = o.commitments.availableBalanceForSendMsat, - canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, + canSend = o.commitments.availableBalanceForSend, + canReceive = o.commitments.availableBalanceForReceive, isPublic = o.commitments.announceChannel)) case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) => @@ -99,7 +95,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardAdd(add, previousFailures) => log.debug(s"received forwarding request for htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId}") - decryptPacket(add, nodeParams.privateKey) match { + decryptPacket(add, nodeParams.privateKey, nodeParams.globalFeatures) match { case Right(p: FinalPayload) => handleFinal(p) match { case Left(cmdFail) => @@ -112,17 +108,21 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Right(r: RelayPayload) => handleRelay(r, channelUpdates, node2channels, previousFailures, nodeParams.chainHash) match { case RelayFailure(cmdFail) => - log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=${r.payload.shortChannelId} reason=${cmdFail.reason}") + log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=${r.payload.outgoingChannelId} reason=${cmdFail.reason}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) case RelaySuccess(selectedShortChannelId, cmdAdd) => log.info(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=$selectedShortChannelId") register ! Register.ForwardShortId(selectedShortChannelId, cmdAdd) } - case Left(badOnion) => + case Left(badOnion: BadOnion) => log.warning(s"couldn't parse onion: reason=${badOnion.message}") - val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, FailureMessageCodecs.failureCode(badOnion), commit = true) + val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true) log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) + case Left(failure) => + log.warning(s"couldn't process onion: reason=${failure.message}") + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), commit = true) + commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Right(add), _, _)))) => @@ -136,8 +136,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Local(id, None) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) - context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) + val result = PaymentFailed(id, paymentHash, Nil) + nodeParams.db.payments.updateOutgoingPayment(result) + context.system.eventStream.publish(result) case Local(_, Some(sender)) => sender ! Status.Failure(addFailed) case Relayed(originChannelId, originHtlcId, _, _) => @@ -157,18 +158,18 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFulfill(fulfill, to, add) => to match { case Local(id, None) => - val feesPaid = MilliSatoshi(0) - context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, - // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, Some(fulfill.paymentPreimage)) - context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // + // we publish the success on the event stream and update the status in paymentDb + val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment + val result = PaymentSent(id, add.paymentHash, fulfill.paymentPreimage, Seq(PaymentSent.PartialPayment(id, add.amountMsat, feesPaid, add.channelId, None))) + nodeParams.db.payments.updateOutgoingPayment(result) + context.system.eventStream.publish(result) case Local(_, Some(sender)) => sender ! fulfill - case Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut) => + case Relayed(originChannelId, originHtlcId, amountIn, amountOut) => val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true) commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) - context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) + context.system.eventStream.publish(PaymentRelayed(amountIn, amountOut, add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) } case ForwardFail(fail, to, add) => @@ -176,8 +177,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Local(id, None) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) - context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) + val result = PaymentFailed(id, add.paymentHash, Nil) + nodeParams.db.payments.updateOutgoingPayment(result) + context.system.eventStream.publish(result) case Local(_, Some(sender)) => sender ! fail case Relayed(originChannelId, originHtlcId, _, _) => @@ -190,8 +192,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Local(id, None) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) - context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) + val result = PaymentFailed(id, add.paymentHash, Nil) + nodeParams.db.payments.updateOutgoingPayment(result) + context.system.eventStream.publish(result) case Local(_, Some(sender)) => sender ! fail case Relayed(originChannelId, originHtlcId, _, _) => @@ -213,58 +216,58 @@ object Relayer extends Logging { // @formatter:off sealed trait NextPayload - case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload - case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionRoutingPacket) extends NextPayload { - val relayFeeMsat: Long = add.amountMsat - payload.amtToForward - val expiryDelta: Long = add.cltvExpiry - payload.outgoingCltvValue + case class FinalPayload(add: UpdateAddHtlc, payload: Onion.FinalPayload) extends NextPayload + case class RelayPayload(add: UpdateAddHtlc, payload: Onion.RelayPayload, nextPacket: OnionRoutingPacket) extends NextPayload { + val relayFeeMsat: MilliSatoshi = add.amountMsat - payload.amountToForward + val expiryDelta: CltvExpiryDelta = add.cltvExpiry - payload.outgoingCltv } // @formatter:on /** - * Decrypt the onion of a received htlc, and find out if the payment is to be relayed, - * or if our node is the last one in the route - * - * @param add incoming htlc - * @param privateKey this node's private key - * @return the payload for the next hop or an error. - */ - def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey): Either[BadOnion, NextPayload] = + * Decrypt the onion of a received htlc, and find out if the payment is to be relayed, or if our node is the last one + * in the route. + * + * @param add incoming htlc + * @param privateKey this node's private key + * @return the payload for the next hop or an error. + */ + def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector): Either[FailureMessage, NextPayload] = Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => - OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { + val codec = if (p.isLastPacket) OnionCodecs.finalPerHopPayloadCodec else OnionCodecs.relayPerHopPayloadCodec + codec.decode(payload.bits) match { + case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !Features.hasVariableLengthOnion(features) => Left(InvalidRealm) case Attempt.Successful(DecodeResult(perHopPayload, remainder)) => if (remainder.nonEmpty) { logger.warn(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") } - if (p.isLastPacket) { - Right(FinalPayload(add, perHopPayload)) - } else { - Right(RelayPayload(add, perHopPayload, nextPacket)) + perHopPayload match { + case finalPayload: Onion.FinalPayload => Right(FinalPayload(add, finalPayload)) + case relayPayload: Onion.RelayPayload => Right(RelayPayload(add, relayPayload, nextPacket)) } - case Attempt.Failure(_) => - // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. - Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + case Attempt.Failure(e: OnionCodecs.MissingRequiredTlv) => Left(e.failureMessage) + // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. + // It's hard to provide tag and offset information from scodec failures, so we currently don't do it. + case Attempt.Failure(_) => Left(InvalidOnionPayload(UInt64(0), 0)) } case Left(badOnion) => Left(badOnion) } /** - * Handle an incoming htlc when we are the last node - * - * @param finalPayload payload - * @return either: - * - a CMD_FAIL_HTLC to be sent back upstream - * - an UpdateAddHtlc to forward - */ - def handleFinal(finalPayload: FinalPayload): Either[CMD_FAIL_HTLC, UpdateAddHtlc] = { - import finalPayload.add - finalPayload.payload match { - case PerHopPayload(_, finalAmountToForward, _) if finalAmountToForward > add.amountMsat => - Left(CMD_FAIL_HTLC(add.id, Right(FinalIncorrectHtlcAmount(add.amountMsat)), commit = true)) - case PerHopPayload(_, _, finalOutgoingCltvValue) if finalOutgoingCltvValue != add.cltvExpiry => - Left(CMD_FAIL_HTLC(add.id, Right(FinalIncorrectCltvExpiry(add.cltvExpiry)), commit = true)) - case _ => - Right(add) + * Handle an incoming htlc when we are the last node + * + * @param p final payload + * @return either: + * - a CMD_FAIL_HTLC to be sent back upstream + * - an UpdateAddHtlc to forward + */ + def handleFinal(p: FinalPayload): Either[CMD_FAIL_HTLC, UpdateAddHtlc] = { + if (p.add.amountMsat < p.payload.amount) { + Left(CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(p.add.amountMsat)), commit = true)) + } else if (p.add.cltvExpiry != p.payload.expiry) { + Left(CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectCltvExpiry(p.add.cltvExpiry)), commit = true)) + } else { + Right(p.add) } } @@ -275,16 +278,16 @@ object Relayer extends Logging { // @formatter:on /** - * Handle an incoming htlc when we are a relaying node - * - * @param relayPayload payload - * @return either: - * - a CMD_FAIL_HTLC to be sent back upstream - * - a CMD_ADD_HTLC to propagate downstream - */ + * Handle an incoming htlc when we are a relaying node + * + * @param relayPayload payload + * @return either: + * - a CMD_FAIL_HTLC to be sent back upstream + * - a CMD_ADD_HTLC to propagate downstream + */ def handleRelay(relayPayload: RelayPayload, channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.Map[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId], previousFailures: Seq[AddHtlcFailed], chainHash: ByteVector32)(implicit log: LoggingAdapter): RelayResult = { import relayPayload._ - log.info(s"relaying htlc #${add.id} paymentHash={} from channelId={} to requestedShortChannelId={} previousAttempts={}", add.paymentHash, add.channelId, relayPayload.payload.shortChannelId, previousFailures.size) + log.info(s"relaying htlc #${add.id} paymentHash={} from channelId={} to requestedShortChannelId={} previousAttempts={}", add.paymentHash, add.channelId, relayPayload.payload.outgoingChannelId, previousFailures.size) val alreadyTried = previousFailures.flatMap(_.channelUpdate).map(_.shortChannelId) selectPreferredChannel(relayPayload, channelUpdates, node2channels, alreadyTried) .flatMap(selectedShortChannelId => channelUpdates.get(selectedShortChannelId).map(_.channelUpdate)) match { @@ -292,7 +295,7 @@ object Relayer extends Logging { // no more channels to try val error = previousFailures // we return the error for the initially requested channel if it exists - .find(_.channelUpdate.map(_.shortChannelId).contains(relayPayload.payload.shortChannelId)) + .find(_.channelUpdate.map(_.shortChannelId).contains(relayPayload.payload.outgoingChannelId)) // otherwise we return the error for the first channel tried .getOrElse(previousFailures.head) RelayFailure(CMD_FAIL_HTLC(add.id, Right(translateError(error)), commit = true)) @@ -302,14 +305,14 @@ object Relayer extends Logging { } /** - * Select a channel to the same node to relay the payment to, that has the lowest balance and is compatible in - * terms of fees, expiry_delta, etc. - * - * If no suitable channel is found we default to the originally requested channel. - */ + * Select a channel to the same node to relay the payment to, that has the lowest 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(relayPayload: RelayPayload, channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.Map[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId], alreadyTried: Seq[ShortChannelId])(implicit log: LoggingAdapter): Option[ShortChannelId] = { import relayPayload.add - val requestedShortChannelId = relayPayload.payload.shortChannelId + val requestedShortChannelId = relayPayload.payload.outgoingChannelId log.debug(s"selecting next channel for htlc #${add.id} paymentHash={} from channelId={} to requestedShortChannelId={} previousAttempts={}", add.paymentHash, add.channelId, requestedShortChannelId, alreadyTried.size) // first we find out what is the next node val nextNodeId_opt = channelUpdates.get(requestedShortChannelId) match { @@ -330,11 +333,11 @@ object Relayer extends Logging { val channelInfo_opt = channelUpdates.get(shortChannelId) val channelUpdate_opt = channelInfo_opt.map(_.channelUpdate) val relayResult = relayOrFail(relayPayload, channelUpdate_opt) - log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.commitments.availableBalanceForSendMsat).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) + log.debug(s"candidate channel for htlc #${add.id} paymentHash=${add.paymentHash}: shortChannelId={} balanceMsat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo_opt.map(_.commitments.availableBalanceForSend).getOrElse(""), channelUpdate_opt.getOrElse(""), relayResult) (shortChannelId, channelInfo_opt, relayResult) } - .collect { case (shortChannelId, Some(channelInfo), _: RelaySuccess) => (shortChannelId, channelInfo.commitments.availableBalanceForSendMsat) } - .filter(_._2 > relayPayload.payload.amtToForward) // we only keep channels that have enough balance to handle this payment + .collect { case (shortChannelId, Some(channelInfo), _: RelaySuccess) => (shortChannelId, channelInfo.commitments.availableBalanceForSend) } + .filter(_._2 > relayPayload.payload.amountToForward) // we only keep channels that have enough balance to handle this payment .toList // needed for ordering .sortBy(_._2) // we want to use the channel with the lowest available balance that can process the payment .headOption match { @@ -356,10 +359,10 @@ object Relayer extends Logging { } /** - * This helper method will tell us if it is not even worth attempting to relay the payment to our local outgoing - * channel, because some parameters don't match with our settings for that channel. In that case we directly fail the - * htlc. - */ + * This helper method will tell us if it is not even worth attempting to relay the payment to our local outgoing + * channel, because some parameters don't match with our settings for that channel. In that case we directly fail the + * htlc. + */ def relayOrFail(relayPayload: RelayPayload, channelUpdate_opt: Option[ChannelUpdate], previousFailures: Seq[AddHtlcFailed] = Seq.empty)(implicit log: LoggingAdapter): RelayResult = { import relayPayload._ channelUpdate_opt match { @@ -367,21 +370,21 @@ object Relayer extends Logging { RelayFailure(CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.channelFlags) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)), commit = true)) - case Some(channelUpdate) if payload.amtToForward < channelUpdate.htlcMinimumMsat => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amtToForward, 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 relayPayload.expiryDelta != channelUpdate.cltvExpiryDelta => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltvValue, channelUpdate)), commit = true)) - case Some(channelUpdate) if relayPayload.relayFeeMsat < nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, payload.amtToForward) => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, channelUpdate)), commit = true)) + case Some(channelUpdate) if relayPayload.relayFeeMsat < nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, payload.amountToForward) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => - RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket, upstream = Right(add), commit = true, previousFailures = previousFailures)) + RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, upstream = Right(add), commit = true, previousFailures = previousFailures)) } } /** - * This helper method translates relaying errors (returned by the downstream outgoing channel) to BOLT 4 standard - * errors that we should return upstream. - */ + * This helper method translates relaying errors (returned by the downstream outgoing channel) to BOLT 4 standard + * errors that we should return upstream. + */ private def translateError(failure: AddHtlcFailed): FailureMessage = { val error = failure.t val channelUpdate_opt = failure.channelUpdate 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 069a33a391..b44cf093c2 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 @@ -19,7 +19,7 @@ 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.eclair.wire._ -import fr.acinq.eclair.{Features, ShortChannelId, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, ShortChannelId, serializationResult} import scodec.bits.{BitVector, ByteVector} import shapeless.HNil @@ -27,8 +27,8 @@ import scala.compat.Platform import scala.concurrent.duration._ /** - * Created by PM on 03/02/2017. - */ + * Created by PM on 03/02/2017. + */ object Announcements { def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: ByteVector, unknownFields: ByteVector): ByteVector = @@ -37,7 +37,7 @@ object Announcements { def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: ByteVector, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: unknownFields :: HNil)))) - def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Option[Long], unknownFields: ByteVector): ByteVector = + def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: unknownFields :: HNil)))) def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { @@ -73,9 +73,8 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: ByteVector, timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { require(alias.length <= 32) - val features = BitVector.fromLong(1 << Features.VARIABLE_LENGTH_ONION_OPTIONAL).bytes val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( @@ -90,37 +89,37 @@ object Announcements { } /** - * BOLT 7: - * The creating node MUST set node-id-1 and node-id-2 to the public keys of the - * two nodes who are operating the channel, such that node-id-1 is the numerically-lesser - * of the two DER encoded keys sorted in ascending numerical order, - * - * @return true if localNodeId is node1 - */ + * BOLT 7: + * The creating node MUST set node-id-1 and node-id-2 to the public keys of the + * two nodes who are operating the channel, such that node-id-1 is the numerically-lesser + * of the two DER encoded keys sorted in ascending numerical order, + * + * @return true if localNodeId is node1 + */ def isNode1(localNodeId: PublicKey, remoteNodeId: PublicKey) = LexicographicalOrdering.isLessThan(localNodeId.value, remoteNodeId.value) /** - * BOLT 7: - * The creating node [...] MUST set the direction bit of flags to 0 if - * the creating node is node-id-1 in that message, otherwise 1. - * - * @return true if the node who sent these flags is node1 - */ + * BOLT 7: + * The creating node [...] MUST set the direction bit of flags to 0 if + * the creating node is node-id-1 in that message, otherwise 1. + * + * @return true if the node who sent these flags is node1 + */ def isNode1(channelFlags: Byte): Boolean = (channelFlags & 1) == 0 /** - * A node MAY create and send a channel_update with the disable bit set to - * signal the temporary unavailability of a channel - * - * @return - */ + * A node MAY create and send a channel_update with the disable bit set to + * signal the temporary unavailability of a channel + * + * @return + */ def isEnabled(channelFlags: Byte): Boolean = (channelFlags & 2) == 0 /** - * This method compares channel updates, ignoring fields that don't matter, like signature or timestamp - * - * @return true if channel updates are "equal" - */ + * This method compares channel updates, ignoring fields that don't matter, like signature or timestamp + * + * @return true if channel updates are "equal" + */ def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = // NB: On Android, we don't compare chain_hash and signature, because they are stripped u1.copy(chainHash = ByteVector32.Zeroes, signature = ByteVector64.Zeroes, timestamp = 0) == u2.copy(chainHash = ByteVector32.Zeroes, signature = ByteVector64.Zeroes, timestamp = 0) // README: on Android we discard chainHash too @@ -129,7 +128,7 @@ object Announcements { def makeChannelFlags(isNode1: Boolean, enable: Boolean): Byte = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(8).toByte() - def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime.milliseconds.toSeconds): ChannelUpdate = { + def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: Long = Platform.currentTime.milliseconds.toSeconds): ChannelUpdate = { val messageFlags = makeMessageFlags(hasOptionChannelHtlcMax = true) // NB: we always support option_channel_htlc_max val channelFlags = makeChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), enable = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala deleted file mode 100644 index b2fb131ac2..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ /dev/null @@ -1,130 +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.router - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.Protocol -import fr.acinq.eclair.ShortChannelId -import scodec.bits.ByteVector - -import scala.annotation.tailrec -import scala.collection.SortedSet - -object ChannelRangeQueries { - - val UNCOMPRESSED_FORMAT = 0.toByte - val ZLIB_FORMAT = 1.toByte - - case class ShortChannelIdsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIds: ByteVector) - - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of short channel id blocks - */ - def encodeShortChannelIds(firstBlockIn: Long, numBlocksIn: Long, shortChannelIds: SortedSet[ShortChannelId], format: Byte, useGzip: Boolean = false): List[ShortChannelIdsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdsBlock(firstBlockIn, numBlocksIn, ByteVector(0))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 7000 - case ZLIB_FORMAT => 12000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeShortChannelIdsSingle(ids, format, useGzip) - ShortChannelIdsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeShortChannelIdsSingle(shortChannelIds: Iterable[ShortChannelId], format: Byte, useGzip: Boolean): ByteVector = { - val bos = new ByteArrayOutputStream() - bos.write(format) - format match { - case UNCOMPRESSED_FORMAT => - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, bos, ByteOrder.BIG_ENDIAN)) - case ZLIB_FORMAT => - val output = if (useGzip) new GZIPOutputStream(bos) else new DeflaterOutputStream(bos) - shortChannelIds.foreach(id => Protocol.writeUInt64(id.toLong, output, ByteOrder.BIG_ENDIAN)) - output.finish() - } - ByteVector.view(bos.toByteArray) - } - - /** - * Decompress a zipped sequence of sorted short channel ids. - * - * @param data - * @return a sorted set of short channel ids - */ - def decodeShortChannelIds(data: ByteVector): (Byte, SortedSet[ShortChannelId], Boolean) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedSet.empty[ShortChannelId], false) else { - val buffer = new Array[Byte](8) - - // read 8 bytes from input - // zipped input stream often returns less bytes than what you want to read - @tailrec - def read8(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 8 - offset) match { - case len if len <= 0 => len - case 8 => 8 - case len if offset + len == 8 => 8 - case len => read8(input, offset + len) - } - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedSet[ShortChannelId]): SortedSet[ShortChannelId] = { - val check = read8(input) - if (check <= 0) acc else loop(input, acc + ShortChannelId(Protocol.uint64(buffer, ByteOrder.BIG_ENDIAN))) - } - - def readAll(useGzip: Boolean) = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT if useGzip => new GZIPInputStream(bis) - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedSet.empty[ShortChannelId]), useGzip) - } - finally { - input.close() - } - } - - try { - readAll(useGzip = false) - } - catch { - case _: Throwable if format == ZLIB_FORMAT => readAll(useGzip = true) - } - } - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala deleted file mode 100644 index 2465a13ca9..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueriesEx.scala +++ /dev/null @@ -1,112 +0,0 @@ -package fr.acinq.eclair.router - -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} -import java.nio.ByteOrder -import java.util.zip.{DeflaterOutputStream, GZIPInputStream, GZIPOutputStream, InflaterInputStream} - -import fr.acinq.bitcoin.{ByteVector32, Protocol} -import fr.acinq.eclair.ShortChannelId -import scodec.bits.ByteVector - -import scala.annotation.tailrec -import scala.collection.SortedSet -import scala.collection.immutable.SortedMap - -object ChannelRangeQueriesEx { - val UNCOMPRESSED_FORMAT = 0.toByte - val ZLIB_FORMAT = 1.toByte - - case class ShortChannelIdAndTimestampsBlock(val firstBlock: Long, val numBlocks: Long, shortChannelIdAndTimestamps: ByteVector) - - /** - * Compressed a sequence of *sorted* short channel id. - * - * @param shortChannelIds must be sorted beforehand - * @return a sequence of encoded short channel ids - */ - def encodeShortChannelIdAndTimestamps(firstBlockIn: Long, numBlocksIn: Long, - shortChannelIds: SortedSet[ShortChannelId], timestamp: ShortChannelId => Long, - format: Byte): List[ShortChannelIdAndTimestampsBlock] = { - if (shortChannelIds.isEmpty) { - // special case: reply with an "empty" block - List(ShortChannelIdAndTimestampsBlock(firstBlockIn, numBlocksIn, ByteVector.fromValidHex("00"))) - } else { - // LN messages must fit in 65 Kb so we split ids into groups to make sure that the output message will be valid - val count = format match { - case UNCOMPRESSED_FORMAT => 4000 - case ZLIB_FORMAT => 8000 // TODO: do something less simplistic... - } - shortChannelIds.grouped(count).map(ids => { - val (firstBlock, numBlocks) = if (ids.isEmpty) (firstBlockIn, numBlocksIn) else { - val firstBlock: Long = ShortChannelId.coordinates(ids.head).blockHeight - val numBlocks: Long = ShortChannelId.coordinates(ids.last).blockHeight - firstBlock + 1 - (firstBlock, numBlocks) - } - val encoded = encodeShortChannelIdAndTimestampsSingle(ids, timestamp, format) - ShortChannelIdAndTimestampsBlock(firstBlock, numBlocks, encoded) - }).toList - } - } - - def encodeShortChannelIdAndTimestampsSingle(shortChannelIds: Iterable[ShortChannelId], timestamp: ShortChannelId => Long, format: Byte): ByteVector = { - val bos = new ByteArrayOutputStream() - bos.write(format) - val out = format match { - case UNCOMPRESSED_FORMAT => bos - case ZLIB_FORMAT => new DeflaterOutputStream(bos) - } - shortChannelIds.foreach(id => { - Protocol.writeUInt64(id.toLong, out, ByteOrder.BIG_ENDIAN) - Protocol.writeUInt32(timestamp(id), out, ByteOrder.BIG_ENDIAN) - }) - out.close() - ByteVector.view(bos.toByteArray) - } - - /** - * Decompress a zipped sequence of sorted [short channel id | timestamp] values. - * - * @param data - * @return a sorted map of short channel id -> timestamp - */ - def decodeShortChannelIdAndTimestamps(data: ByteVector): (Byte, SortedMap[ShortChannelId, Long]) = { - val format = data.head - if (data.tail.isEmpty) (format, SortedMap.empty[ShortChannelId, Long]) else { - val buffer = new Array[Byte](12) - - // read 12 bytes from input - // zipped input stream often returns less bytes than what you want to read - @tailrec - def read12(input: InputStream, offset: Int = 0): Int = input.read(buffer, offset, 12 - offset) match { - case len if len <= 0 => len - case 12 => 12 - case len if offset + len == 12 => 12 - case len => read12(input, offset + len) - } - - - // read until there's nothing left - @tailrec - def loop(input: InputStream, acc: SortedMap[ShortChannelId, Long]): SortedMap[ShortChannelId, Long] = { - val check = read12(input) - if (check <= 0) acc else loop(input, acc + (ShortChannelId(Protocol.uint64(buffer.take(8), ByteOrder.BIG_ENDIAN)) -> Protocol.uint32(buffer.drop(8), ByteOrder.BIG_ENDIAN))) - } - - def readAll() = { - val bis = new ByteArrayInputStream(data.tail.toArray) - val input = format match { - case UNCOMPRESSED_FORMAT => bis - case ZLIB_FORMAT => new InflaterInputStream(bis) - } - try { - (format, loop(input, SortedMap.empty[ShortChannelId, Long])) - } - finally { - input.close() - } - } - - readAll() - } - } -} 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 178036a3ae..23a19e65a7 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 @@ -23,15 +23,27 @@ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.ChannelUpdate +import scala.collection.immutable.SortedMap import scala.collection.mutable object Graph { // @formatter:off - // A compound weight for an edge, weight is obtained with (cost X factor),'cost' contains the actual amount+fees in millisatoshi, 'cltvCumulative' the total CLTV necessary to reach this edge - case class RichWeight(cost: Long, length: Int, cltv: Int, weight: Double) extends Ordered[RichWeight] { - override def compare(that: RichWeight): Int = this.weight.compareTo(that.weight) + /** + * The cumulative weight of a set of edges (path in the graph). + * + * @param cost amount to send to the recipient + each edge's fees + * @param length number of edges in the path + * @param cltv sum of each edge's cltv + * @param weight cost multiplied by a factor based on heuristics (see [[WeightRatios]]). + */ + case class RichWeight(cost: MilliSatoshi, length: Int, cltv: CltvExpiryDelta, weight: Double) extends Ordered[RichWeight] { + override def compare(that: RichWeight): Int = this.weight.compareTo(that.weight) } + /** + * We use heuristics to calculate the weight of an edge based on channel age, cltv delta and capacity. + * We favor older channels, with bigger capacity and small cltv delta. + */ case class WeightRatios(cltvDeltaFactor: Double, ageFactor: Double, capacityFactor: Double) { require(0 < cltvDeltaFactor + ageFactor + capacityFactor && cltvDeltaFactor + ageFactor + capacityFactor <= 1, "The sum of heuristics ratios must be between 0 and 1 (included)") } @@ -40,9 +52,9 @@ object Graph { // @formatter:on /** - * This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with - * the same weight we distinguish them by their public key. See https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html - */ + * This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with + * the same weight we distinguish them by their public key. See https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html + */ object QueueComparator extends Ordering[WeightedNode] { override def compare(x: WeightedNode, y: WeightedNode): Int = { val weightCmp = x.weight.compareTo(y.weight) @@ -56,24 +68,24 @@ object Graph { } /** - * Yen's algorithm to find the k-shortest (loopless) paths in a graph, uses dijkstra as search algo. Is guaranteed to terminate finding - * at most @pathsToFind paths sorted by cost (the cheapest is in position 0). - * - * @param graph - * @param sourceNode - * @param targetNode - * @param amountMsat - * @param pathsToFind - * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path - * @param currentBlockHeight the height of the chain tip (latest block) - * @param boundaries a predicate function that can be used to impose limits on the outcome of the search - * @return - */ + * Yen's algorithm to find the k-shortest (loop-less) paths in a graph, uses dijkstra as search algo. Is guaranteed to terminate finding + * at most @pathsToFind paths sorted by cost (the cheapest is in position 0). + * + * @param graph graph representing the whole network + * @param sourceNode sender node (payer) + * @param targetNode target node (final recipient) + * @param amount amount to send to the last node + * @param pathsToFind number of distinct paths to be returned + * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path + * @param currentBlockHeight the height of the chain tip (latest block) + * @param boundaries a predicate function that can be used to impose limits on the outcome of the search + */ def yenKshortestPaths(graph: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, ignoredEdges: Set[ChannelDesc], + ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], pathsToFind: Int, wr: Option[WeightRatios], @@ -88,9 +100,9 @@ object Graph { val candidates = new mutable.PriorityQueue[WeightedPath] // find the shortest path, k = 0 - val initialWeight = RichWeight(cost = amountMsat, 0, 0, 0) - val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, extraEdges, initialWeight, boundaries, currentBlockHeight, wr) - shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amountMsat, isPartial = false, currentBlockHeight, wr)) + val initialWeight = RichWeight(cost = amount, 0, CltvExpiryDelta(0), 0) + val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, initialWeight, boundaries, currentBlockHeight, wr) + shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amount, isPartial = false, currentBlockHeight, wr)) // avoid returning a list with an empty path if (shortestPath.isEmpty) return Seq.empty @@ -110,7 +122,7 @@ object Graph { // select the sub-path from the source to the spur node of the k-th previous shortest path val rootPathEdges = if (i == 0) prevShortestPath.head :: Nil else prevShortestPath.take(i) - val rootPathWeight = pathWeight(rootPathEdges, amountMsat, isPartial = true, currentBlockHeight, wr) + val rootPathWeight = pathWeight(rootPathEdges, amount, isPartial = true, currentBlockHeight, wr) // links to be removed that are part of the previous shortest path and which share the same root path val edgesToIgnore = shortestPaths.flatMap { weightedPath => @@ -125,7 +137,7 @@ object Graph { val returningEdges = rootPathEdges.lastOption.map(last => graph.getEdgesBetween(last.desc.b, last.desc.a)).toSeq.flatten.map(_.desc) // find the "spur" path, a sub-path going from the spur edge to the target avoiding previously found sub-paths - val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, ignoredEdges ++ edgesToIgnore.toSet ++ returningEdges.toSet, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr) + val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, ignoredEdges ++ edgesToIgnore.toSet ++ returningEdges.toSet, ignoredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr) // if there wasn't a path the spur will be empty if (spurPath.nonEmpty) { @@ -136,7 +148,7 @@ object Graph { case false => rootPathEdges ++ spurPath } - val candidatePath = WeightedPath(totalPath, pathWeight(totalPath, amountMsat, isPartial = false, currentBlockHeight, wr)) + val candidatePath = WeightedPath(totalPath, pathWeight(totalPath, amount, isPartial = false, currentBlockHeight, wr)) if (boundaries(candidatePath.weight) && !shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath)) { candidates.enqueue(candidatePath) @@ -159,25 +171,25 @@ object Graph { } /** - * Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes - * the shortest path from the target to the source (this is because we want to calculate the weight of the - * edges correctly). The graph @param g is optimized for querying the incoming edges given a vertex. - * - * @param g the graph on which will be performed the search - * @param sourceNode the starting node of the path we're looking for - * @param targetNode the destination node of the path - * @param ignoredEdges a list of edges we do not want to consider - * @param extraEdges a list of extra edges we want to consider but are not currently in the graph - * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path - * @param currentBlockHeight the height of the chain tip (latest block) - * @param boundaries a predicate function that can be used to impose limits on the outcome of the search - * @return - */ - + * Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes + * the shortest path from the target to the source (this is because we want to calculate the weight of the + * edges correctly). The graph @param g is optimized for querying the incoming edges given a vertex. + * + * @param g the graph on which will be performed the search + * @param sourceNode the starting node of the path we're looking for + * @param targetNode the destination node of the path + * @param ignoredEdges a list of edges we do not want to consider + * @param extraEdges a list of extra edges we want to consider but are not currently in the graph + * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path + * @param currentBlockHeight the height of the chain tip (latest block) + * @param boundaries a predicate function that can be used to impose limits on the outcome of the search + * @return + */ def dijkstraShortestPath(g: DirectedGraph, sourceNode: PublicKey, targetNode: PublicKey, ignoredEdges: Set[ChannelDesc], + ignoredVertices: Set[PublicKey], extraEdges: Set[GraphEdge], initialWeight: RichWeight, boundaries: RichWeight => Boolean, @@ -232,12 +244,11 @@ object Graph { if (edge.update.htlcMaximumMsat.forall(newMinimumKnownWeight.cost <= _) && newMinimumKnownWeight.cost >= edge.update.htlcMinimumMsat && boundaries(newMinimumKnownWeight) && // check if this neighbor edge would break off the 'boundaries' - !ignoredEdges.contains(edge.desc) + !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor) ) { - // we call containsKey first because "getOrDefault" is not available in JDK7 val neighborCost = weight.containsKey(neighbor) match { - case false => RichWeight(Long.MaxValue, Int.MaxValue, Int.MaxValue, Double.MaxValue) + case false => RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), Double.MaxValue) case true => weight.get(neighbor) } @@ -281,7 +292,7 @@ object Graph { private def edgeWeight(edge: GraphEdge, prev: RichWeight, isNeighborTarget: Boolean, currentBlockHeight: Long, weightRatios: Option[WeightRatios]): RichWeight = weightRatios match { case None => val edgeCost = if (isNeighborTarget) prev.cost else edgeFeeCost(edge, prev.cost) - RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + edge.update.cltvExpiryDelta, weight = edgeCost) + RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + edge.update.cltvExpiryDelta, weight = edgeCost.toLong) case Some(wr) => import RoutingHeuristics._ @@ -291,11 +302,11 @@ object Graph { val ageFactor = normalize(channelBlockHeight, min = currentBlockHeight - BLOCK_TIME_TWO_MONTHS, max = currentBlockHeight) // Every edge is weighted by channel capacity, larger channels add less weight - val edgeMaxCapacity = edge.update.htlcMaximumMsat.getOrElse(CAPACITY_CHANNEL_LOW_MSAT) - val capFactor = 1 - normalize(edgeMaxCapacity, CAPACITY_CHANNEL_LOW_MSAT, CAPACITY_CHANNEL_HIGH_MSAT) + val edgeMaxCapacity = edge.update.htlcMaximumMsat.getOrElse(CAPACITY_CHANNEL_LOW) + val capFactor = 1 - normalize(edgeMaxCapacity.toLong, CAPACITY_CHANNEL_LOW.toLong, CAPACITY_CHANNEL_HIGH.toLong) - // Every edge is weighted by its clvt-delta value, normalized - val channelCltvDelta = edge.update.cltvExpiryDelta + // Every edge is weighted by its cltv-delta value, normalized + val channelCltvDelta = edge.update.cltvExpiryDelta.toInt val cltvFactor = normalize(channelCltvDelta, CLTV_LOW, CLTV_HIGH) // NB 'edgeCost' includes the amount to be sent plus the fees that must be paid to traverse this @param edge @@ -303,33 +314,33 @@ object Graph { // NB we're guaranteed to have weightRatios and factors > 0 val factor = (cltvFactor * wr.cltvDeltaFactor) + (ageFactor * wr.ageFactor) + (capFactor * wr.capacityFactor) - val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost * factor + val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost.toLong * factor RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + channelCltvDelta, weight = edgeWeight) } /** - * This forces channel_update(s) with fees=0 to have a minimum of 1msat for the baseFee. Note that - * the update is not being modified and the result of the route computation will still have the update - * with fees=0 which is what will be used to build the onion. - * - * @param edge the edge for which we want to compute the weight - * @param amountWithFees the value that this edge will have to carry along - * @return the new amount updated with the necessary fees for this edge - */ - private def edgeFeeCost(edge: GraphEdge, amountWithFees: Long): Long = { - if(edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseMsat = 1, proportional = 0, amountWithFees) + * This forces channel_update(s) with fees=0 to have a minimum of 1msat for the baseFee. Note that + * the update is not being modified and the result of the route computation will still have the update + * with fees=0 which is what will be used to build the onion. + * + * @param edge the edge for which we want to compute the weight + * @param amountWithFees the value that this edge will have to carry along + * @return the new amount updated with the necessary fees for this edge + */ + private def edgeFeeCost(edge: GraphEdge, amountWithFees: MilliSatoshi): MilliSatoshi = { + if (edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseFee = 1 msat, proportionalFee = 0, amountWithFees) else amountWithFees + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountWithFees) } private def edgeHasZeroFee(edge: GraphEdge): Boolean = { - edge.update.feeBaseMsat == 0 && edge.update.feeProportionalMillionths == 0 + edge.update.feeBaseMsat.toLong == 0 && edge.update.feeProportionalMillionths == 0 } // Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees) - def pathWeight(path: Seq[GraphEdge], amountMsat: Long, isPartial: Boolean, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { - path.drop(if (isPartial) 0 else 1).foldRight(RichWeight(amountMsat, 0, 0, 0)) { (edge, prev) => - edgeWeight(edge, prev, false, currentBlockHeight, wr) + def pathWeight(path: Seq[GraphEdge], amountMsat: MilliSatoshi, isPartial: Boolean, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { + path.drop(if (isPartial) 0 else 1).foldRight(RichWeight(amountMsat, 0, CltvExpiryDelta(0), 0)) { (edge, prev) => + edgeWeight(edge, prev, isNeighborTarget = false, currentBlockHeight, wr) } } @@ -340,22 +351,17 @@ object Graph { val BLOCK_TIME_TWO_MONTHS = 8640 // Low/High bound for channel capacity - val CAPACITY_CHANNEL_LOW_MSAT = 1000 * 1000L // 1000 sat - val CAPACITY_CHANNEL_HIGH_MSAT = Channel.MAX_FUNDING_SATOSHIS * 1000L + val CAPACITY_CHANNEL_LOW = (1000 sat).toMilliSatoshi + val CAPACITY_CHANNEL_HIGH = Channel.MAX_FUNDING.toMilliSatoshi // Low/High bound for CLTV channel value val CLTV_LOW = 9 val CLTV_HIGH = 2016 /** - * Normalize the given value between (0, 1). If the @param value is outside the min/max window we flatten it to something very close to the - * extremes but always bigger than zero so it's guaranteed to never return zero - * - * @param value - * @param min - * @param max - * @return - */ + * Normalize the given value between (0, 1). If the @param value is outside the min/max window we flatten it to something very close to the + * extremes but always bigger than zero so it's guaranteed to never return zero + */ def normalize(value: Double, min: Double, max: Double) = { if (value <= min) 0.00001D else if (value > max) 0.99999D @@ -364,16 +370,16 @@ object Graph { } /** - * A graph data structure that uses the adjacency lists, stores the incoming edges of the neighbors - */ + * A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors + */ object GraphStructure { /** - * Representation of an edge of the graph - * - * @param desc channel description - * @param update channel info - */ + * Representation of an edge of the graph + * + * @param desc channel description + * @param update channel info + */ case class GraphEdge(desc: ChannelDesc, update: ChannelUpdate) case class DirectedGraph(private val vertices: Map[PublicKey, List[GraphEdge]]) { @@ -385,13 +391,12 @@ object Graph { } /** - * Adds and edge to the graph, if one of the two vertices is not found, it will be created. - * - * @param edge the edge that is going to be added to the graph - * @return a new graph containing this edge - */ + * Adds an edge to the graph. If one of the two vertices is not found it will be created. + * + * @param edge the edge that is going to be added to the graph + * @return a new graph containing this edge + */ def addEdge(edge: GraphEdge): DirectedGraph = { - val vertexIn = edge.desc.a val vertexOut = edge.desc.b @@ -405,12 +410,12 @@ object Graph { } /** - * Removes the edge corresponding to the given pair channel-desc/channel-update, - * NB: this operation does NOT remove any vertex - * - * @param desc the channel description associated to the edge that will be removed - * @return - */ + * Removes the edge corresponding to the given pair channel-desc/channel-update, + * NB: this operation does NOT remove any vertex + * + * @param desc the channel description associated to the edge that will be removed + * @return a new graph without this edge + */ def removeEdge(desc: ChannelDesc): DirectedGraph = { containsEdge(desc) match { case true => DirectedGraph(vertices.updated(desc.b, vertices(desc.b).filterNot(_.desc == desc))) @@ -423,9 +428,8 @@ object Graph { } /** - * @param edge - * @return For edges to be considered equal they must have the same in/out vertices AND same shortChannelId - */ + * @return For edges to be considered equal they must have the same in/out vertices AND same shortChannelId + */ def getEdge(edge: GraphEdge): Option[GraphEdge] = getEdge(edge.desc) def getEdge(desc: ChannelDesc): Option[GraphEdge] = { @@ -435,10 +439,10 @@ object Graph { } /** - * @param keyA the key associated with the starting vertex - * @param keyB the key associated with the ending vertex - * @return all the edges going from keyA --> keyB (there might be more than one if it refers to different shortChannelId) - */ + * @param keyA the key associated with the starting vertex + * @param keyB the key associated with the ending vertex + * @return all the edges going from keyA --> keyB (there might be more than one if there are multiple channels) + */ def getEdgesBetween(keyA: PublicKey, keyB: PublicKey): Seq[GraphEdge] = { vertices.get(keyB) match { case None => Seq.empty @@ -447,31 +451,23 @@ object Graph { } /** - * The the incoming edges for vertex @param keyB - * - * @param keyB - * @return - */ + * @param keyB the key associated with the target vertex + * @return all edges incoming to that vertex + */ def getIncomingEdgesOf(keyB: PublicKey): Seq[GraphEdge] = { vertices.getOrElse(keyB, List.empty) } /** - * Removes a vertex and all it's associated edges (both incoming and outgoing) - * - * @param key - * @return - */ + * Removes a vertex and all its associated edges (both incoming and outgoing) + */ def removeVertex(key: PublicKey): DirectedGraph = { DirectedGraph(removeEdges(getIncomingEdgesOf(key).map(_.desc)).vertices - key) } /** - * Adds a new vertex to the graph, starting with no edges - * - * @param key - * @return - */ + * Adds a new vertex to the graph, starting with no edges + */ def addVertex(key: PublicKey): DirectedGraph = { vertices.get(key) match { case None => DirectedGraph(vertices + (key -> List.empty)) @@ -480,35 +476,32 @@ object Graph { } /** - * Note this operation will traverse all edges in the graph (expensive) - * - * @param key - * @return a list of the outgoing edges of vertex @param key, if the edge doesn't exists an empty list is returned - */ + * Note this operation will traverse all edges in the graph (expensive) + * + * @return a list of the outgoing edges of the given vertex. If the vertex doesn't exists an empty list is returned. + */ def edgesOf(key: PublicKey): Seq[GraphEdge] = { edgeSet().filter(_.desc.a == key).toSeq } /** - * @return the set of all the vertices in this graph - */ + * @return the set of all the vertices in this graph + */ def vertexSet(): Set[PublicKey] = vertices.keySet /** - * @return an iterator of all the edges in this graph - */ + * @return an iterator of all the edges in this graph + */ def edgeSet(): Iterable[GraphEdge] = vertices.values.flatten /** - * @param key - * @return true if this graph contain a vertex with this key, false otherwise - */ + * @return true if this graph contain a vertex with this key, false otherwise + */ def containsVertex(key: PublicKey): Boolean = vertices.contains(key) /** - * @param desc - * @return true if this edge desc is in the graph. For edges to be considered equal they must have the same in/out vertices AND same shortChannelId - */ + * @return true if this edge desc is in the graph. For edges to be considered equal they must have the same in/out vertices AND same shortChannelId + */ def containsEdge(desc: ChannelDesc): Boolean = { vertices.get(desc.b) match { case None => false @@ -525,29 +518,45 @@ object Graph { object DirectedGraph { - // convenience constructors + // @formatter:off def apply(): DirectedGraph = new DirectedGraph(Map()) - def apply(key: PublicKey): DirectedGraph = new DirectedGraph(Map(key -> List.empty)) - def apply(edge: GraphEdge): DirectedGraph = new DirectedGraph(Map()).addEdge(edge.desc, edge.update) - def apply(edges: Seq[GraphEdge]): DirectedGraph = { - makeGraph(edges.map(e => e.desc -> e.update).toMap) + DirectedGraph().addEdges(edges.map(e => (e.desc, e.update))) } + // @formatter:on - // optimized constructor - def makeGraph(descAndUpdates: Map[ChannelDesc, ChannelUpdate]): DirectedGraph = { - + /** + * This is the recommended way of creating the network graph. + * We don't include private channels: they would bloat the graph without providing any value (if they are private + * they likely don't want to be involved in routing other people's payments). + * The only private channels we know are ours: we should check them to see if our destination can be reached in a + * single hop via a private channel before using the public network graph. + * + * @param channels map of all known public channels in the network. + */ + def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new {} with mutable.HashMap[PublicKey, List[GraphEdge]] { - override def initialSize: Int = descAndUpdates.size + 1 + override def initialSize: Int = channels.size + 1 } // add all the vertices and edges in one go - descAndUpdates.foreach { case (desc, update) => - // create or update vertex (desc.b) and update its neighbor - mutableMap.put(desc.b, GraphEdge(desc, update) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) + channels.values.foreach { channel => + channel.update_1_opt.foreach { u1 => + val desc1 = Router.getDesc(u1, channel.ann) + addDescToMap(desc1, u1) + } + + channel.update_2_opt.foreach { u2 => + val desc2 = Router.getDesc(u2, channel.ann) + addDescToMap(desc2, u2) + } + } + + def addDescToMap(desc: ChannelDesc, u: ChannelUpdate): Unit = { + mutableMap.put(desc.b, GraphEdge(desc, u) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) mutableMap.get(desc.a) match { case None => mutableMap += desc.a -> List.empty[GraphEdge] case _ => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala new file mode 100644 index 0000000000..0321c0dd55 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkStats.scala @@ -0,0 +1,63 @@ +/* + * 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.router + +import com.google.common.math.Quantiles.percentiles +import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair.wire.ChannelUpdate +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi} + +import scala.collection.JavaConverters._ + +/** + * Created by t-bast on 30/08/2019. + */ + +case class Stats[T](median: T, percentile5: T, percentile10: T, percentile25: T, percentile75: T, percentile90: T, percentile95: T) + +object Stats { + def apply[T](values: Seq[Long], fromDouble: Double => T): Stats[T] = { + require(values.nonEmpty, "can't compute stats on empty values") + val stats = percentiles().indexes(5, 10, 25, 50, 75, 90, 95).compute(values.map(java.lang.Long.valueOf).asJavaCollection) + Stats(fromDouble(stats.get(50)), fromDouble(stats.get(5)), fromDouble(stats.get(10)), fromDouble(stats.get(25)), fromDouble(stats.get(75)), fromDouble(stats.get(90)), fromDouble(stats.get(95))) + } +} + +case class NetworkStats(channels: Int, nodes: Int, capacity: Stats[Satoshi], cltvExpiryDelta: Stats[CltvExpiryDelta], feeBase: Stats[MilliSatoshi], feeProportional: Stats[Long]) + +object NetworkStats { + /** + * Computes various network statistics (expensive). + * Network statistics won't change noticeably very quickly, so this should not be re-computed too often. + */ + def apply(publicChannels: Seq[PublicChannel]): Option[NetworkStats] = { + // We need at least one channel update to be able to compute stats. + if (publicChannels.isEmpty || publicChannels.flatMap(pc => getChannelUpdateField(pc, _ => true)).isEmpty) { + None + } else { + val capacityStats = Stats(publicChannels.map(_.capacity.toLong), d => Satoshi(d.toLong)) + val cltvStats = Stats(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.cltvExpiryDelta.toInt.toLong)), d => CltvExpiryDelta(d.toInt)) + val feeBaseStats = Stats(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.feeBaseMsat.toLong)), d => MilliSatoshi(d.toLong)) + val feeProportionalStats = Stats(publicChannels.flatMap(pc => getChannelUpdateField(pc, u => u.feeProportionalMillionths)), d => d.toLong) + val nodes = publicChannels.flatMap(pc => pc.ann.nodeId1 :: pc.ann.nodeId2 :: Nil).toSet.size + Some(NetworkStats(publicChannels.size, nodes, capacityStats, cltvStats, feeBaseStats, feeProportionalStats)) + } + } + + private def getChannelUpdateField[T](pc: PublicChannel, f: ChannelUpdate => T): Seq[T] = (pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).map(f) + +} 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 282ab7293b..158a092ae6 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 @@ -19,10 +19,10 @@ package fr.acinq.eclair.router import akka.Done import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ @@ -31,96 +31,144 @@ import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidAnnouncement, InvalidSigna 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.{RichWeight, WeightRatios} +import fr.acinq.eclair.router.Graph.{RichWeight, RoutingHeuristics, WeightRatios} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ +import kamon.Kamon +import kamon.context.Context import scodec.bits.ByteVector +import shapeless.HNil -import scala.collection.immutable.{SortedMap, TreeMap} +import scala.annotation.tailrec +import scala.collection.immutable.SortedMap import scala.collection.{SortedSet, mutable} import scala.compat.Platform import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Promise} import scala.util.{Random, Try} -// @formatter:off +/** + * Created by PM on 24/05/2016. + */ case class RouterConf(randomizeRouteSelection: Boolean, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, - searchMaxFeeBaseSat: Long, + networkStatsRefreshInterval: FiniteDuration, + requestNodeAnnouncements: Boolean, + encodingType: EncodingType, + channelRangeChunkSize: Int, + channelQueryChunkSize: Int, + searchMaxFeeBase: Satoshi, searchMaxFeePct: Double, searchMaxRouteLength: Int, - searchMaxCltv: Int, + searchMaxCltv: CltvExpiryDelta, searchHeuristicsEnabled: Boolean, searchRatioCltv: Double, searchRatioChannelAge: Double, searchRatioChannelCapacity: Double) +// @formatter:off case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) +case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { + update_1_opt.foreach(u => assert(Announcements.isNode1(u.channelFlags))) + update_2_opt.foreach(u => assert(!Announcements.isNode1(u.channelFlags))) + + def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) ann.nodeId1 else ann.nodeId2 + + def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + + def updateChannelUpdateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) +} +case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate]) { + val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) + + def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (Announcements.isNode1(u.channelFlags)) nodeId1 else nodeId2 + + def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt + + def updateChannelUpdateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) +} +// @formatter:on + +case class AssistedChannel(extraHop: ExtraHop, nextNodeId: PublicKey, htlcMaximum: MilliSatoshi) + case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) -case class RouteParams(randomize: Boolean, maxFeeBaseMsat: Long, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: Int, ratios: Option[WeightRatios]) + +case class RouteParams(randomize: Boolean, maxFeeBase: MilliSatoshi, maxFeePct: Double, routeMaxLength: Int, routeMaxCltv: CltvExpiryDelta, ratios: Option[WeightRatios]) + case class RouteRequest(source: PublicKey, target: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[ChannelDesc] = Set.empty, routeParams: Option[RouteParams] = None) -case class FinalizeRoute(hops:Seq[PublicKey]) +case class FinalizeRoute(hops: Seq[PublicKey]) + case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) { - require(hops.size > 0, "route cannot be empty") + require(hops.nonEmpty, "route cannot be empty") } -case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) + +// @formatter:off +/** This is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed) */ +case class ExcludeChannel(desc: ChannelDesc) case class LiftChannelExclusion(desc: ChannelDesc) -case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef) -case class SendChannelQueryEx(remoteNodeId: PublicKey, to: ActorRef) +// @formatter:on + +case class SendChannelQuery(remoteNodeId: PublicKey, to: ActorRef, flags_opt: Option[QueryChannelRangeTlv]) + +case object GetNetworkStats + case object GetRoutingState -case class RoutingState(channels: Iterable[ChannelAnnouncement], updates: Iterable[ChannelUpdate], nodes: Iterable[NodeAnnouncement]) + +case class RoutingState(channels: Iterable[PublicChannel], nodes: Iterable[NodeAnnouncement]) + case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) + case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]]) -case class Sync(missing: SortedSet[ShortChannelId], totalMissingCount: Int, outdated: SortedSet[ShortChannelId] = SortedSet.empty[ShortChannelId], totalOutdatedCount: Int = 0) +case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long) + +case class Sync(pending: List[RoutingMessage], total: Int) case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, ChannelAnnouncement], - updates: Map[ChannelDesc, ChannelUpdate], + channels: SortedMap[ShortChannelId, PublicChannel], + stats: Option[NetworkStats], stash: Stash, awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // 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, PublicKey], // short_channel_id -> node_id - privateUpdates: Map[ChannelDesc, ChannelUpdate], + privateChannels: Map[ShortChannelId, PrivateChannel], // short_channel_id -> node_id excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graph: DirectedGraph, sync: Map[PublicKey, Sync] // 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 + // for which we have not yet received an 'end' message ) +// @formatter:off sealed trait State case object NORMAL extends State case object TickBroadcast case object TickPruneStaleChannels - +case object TickComputeNetworkStats // @formatter:on -/** - * Created by PM on 24/05/2016. - */ - -class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Promise[Done]] = None) extends FSMDiagnosticActorLogging[State, Data] { +class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Promise[Done]] = None) extends FSMDiagnosticActorLogging[State, Data] { import Router._ import ExecutionContext.Implicits.global + // we pass these to helpers classes so that they have the logging context + implicit def implicitLog: LoggingAdapter = log + context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) setTimer(TickBroadcast.toString, TickBroadcast, nodeParams.routerConf.routerBroadcastInterval, repeat = true) setTimer(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 hour, repeat = true) - - val SHORTID_WINDOW = 100 + setTimer(TickComputeNetworkStats.toString, TickComputeNetworkStats, nodeParams.routerConf.networkStatsRefreshInterval, repeat = true) val defaultRouteParams = getDefaultRouteParams(nodeParams.routerConf) @@ -130,22 +178,16 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom log.info("loading network announcements from db...") // On Android, we discard the node announcements val channels = db.listChannels() - val updates = db.listChannelUpdates() - log.info("loaded from db: channels={} nodes={} updates={}", channels.size, 0, updates.size) - - val initChannels = channels.keys.foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = updates.map { u => - val desc = getDesc(u, initChannels(u.shortChannelId)) - desc -> u - }.toMap + log.info("loaded from db: channels={}", channels.size) + val initChannels = channels // this will be used to calculate routes - val graph = DirectedGraph.makeGraph(initChannelUpdates) + val graph = DirectedGraph.makeGraph(initChannels) // On Android we don't watch the funding tx outputs of public channels log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(Map.empty, initChannels, initChannelUpdates, Stash(Map.empty, Map.empty), awaiting = Map.empty, privateChannels = Map.empty, privateUpdates = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + startWith(NORMAL, Data(Map.empty, initChannels, None, Stash(Map.empty, Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) } when(NORMAL) { @@ -172,7 +214,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // 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.info("adding unannounced local channel to remote={} shortChannelId={}", remoteNodeId, shortChannelId) - stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> remoteNodeId))) + stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(nodeParams.nodeId, remoteNodeId, None, None)))) } } @@ -192,7 +234,15 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom .removeEdge(desc1) .removeEdge(desc2) // and we remove the channel and channel_update from our state - stay using d.copy(privateChannels = d.privateChannels - shortChannelId, privateUpdates = d.privateUpdates - desc1 - desc2, graph = graph1) + stay using d.copy(privateChannels = d.privateChannels - shortChannelId, graph = graph1) + } else { + stay + } + + case Event(SyncProgress(progress), d: Data) => + if (d.stats.isEmpty && progress == 1.0 && d.channels.nonEmpty) { + log.info("initial routing sync done: computing network statistics") + stay using d.copy(stats = NetworkStats(d.channels.values.toSeq)) } else { stay } @@ -201,7 +251,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay // ignored on Android case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d) if d.channels.contains(shortChannelId) => - val lostChannel = d.channels(shortChannelId) + 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 @@ -209,49 +259,50 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // 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 - .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId1, lostChannel.nodeId2)) - .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId2, lostChannel.nodeId1)) + // we also need to remove updates from the graph + val graph1 = d.graph + .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId1, lostChannel.nodeId2)) + .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId2, lostChannel.nodeId1)) context.system.eventStream.publish(ChannelLost(shortChannelId)) lostNodes.foreach { - case nodeId => + nodeId => log.info("pruning nodeId={} (spent)", nodeId) db.removeNode(nodeId) context.system.eventStream.publish(NodeLost(nodeId)) } - stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, updates = d.updates.filterKeys(_.shortChannelId != shortChannelId), graph = graph1) + stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graph = graph1) case Event(TickBroadcast, d) => // On Android we don't rebroadcast announcements stay + case Event(TickComputeNetworkStats, d) if d.channels.nonEmpty => + log.info("re-computing network statistics") + stay using d.copy(stats = NetworkStats(d.channels.values.toSeq)) + case Event(TickPruneStaleChannels, d) => // first we select channels that we will prune - val staleChannels = getStaleChannels(d.channels.values, d.updates) - // then we clean up the related channel updates - val staleUpdates = staleChannels.map(d.channels).flatMap(c => Seq(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2), ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1))) - // finally we remove nodes that aren't tied to any channels anymore (and deduplicate them) - val potentialStaleNodes = staleChannels.map(d.channels).flatMap(c => Set(c.nodeId1, c.nodeId2)).toSet - val channels1 = d.channels -- staleChannels + val staleChannels = getStaleChannels(d.channels.values, nodeParams.currentBlockHeight) + val staleChannelIds = staleChannels.map(_.ann.shortChannelId) + val channels1 = d.channels -- staleChannelIds // let's clean the db and send the events - db.removeChannels(staleChannels) // NB: this also removes channel updates + db.removeChannels(staleChannelIds) // NB: this also removes channel updates // On Android we don't track pruned channels in our db - staleChannels.foreach { shortChannelId => + staleChannelIds.foreach { shortChannelId => log.info("pruning shortChannelId={} (stale)", shortChannelId) context.system.eventStream.publish(ChannelLost(shortChannelId)) } - // we also need to remove updates from the graph + val staleChannelsToRemove = new mutable.MutableList[ChannelDesc] - staleChannels.map(d.channels).foreach(ca => { - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2) - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1) + staleChannels.foreach(ca => { + staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId1, ca.ann.nodeId2) + staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId2, ca.ann.nodeId1) }) val graph1 = d.graph.removeEdges(staleChannelsToRemove) - stay using d.copy(channels = channels1, updates = d.updates -- staleUpdates, graph = graph1) + stay using d.copy(channels = channels1, graph = graph1) case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) => val banDuration = nodeParams.routerConf.channelExcludeDuration @@ -268,15 +319,16 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay case Event('channels, d) => - sender ! d.channels.values + sender ! d.channels.values.map(_.ann) stay - case Event('updates, d) => - sender ! (d.updates ++ d.privateUpdates).values + case Event('channelsMap, d) => + sender ! d.channels stay - case Event('updatesMap, d) => - sender ! (d.updates ++ d.privateUpdates) + case Event('updates, d) => + val updates: Iterable[ChannelUpdate] = d.channels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) ++ d.privateChannels.values.flatMap(d => d.update_1_opt ++ d.update_2_opt) + sender ! updates stay case Event('data, d) => @@ -284,35 +336,38 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom stay case Event(FinalizeRoute(partialHops), d) => - // split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs, then select the largest edge between them - val edges = partialHops.sliding(2).map { case List(v1, v2) => d.graph.getEdgesBetween(v1, v2).maxBy(_.update.htlcMaximumMsat) } - val hops = edges.map(d => Hop(d.desc.a, d.desc.b, d.update)).toSeq - sender ! RouteResponse(hops, Set.empty, Set.empty) + // split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs + partialHops.sliding(2).map { case List(v1, v2) => d.graph.getEdgesBetween(v1, v2) }.toList match { + case edges if edges.nonEmpty && edges.forall(_.nonEmpty) => + val selectedEdges = edges.map(_.maxBy(_.update.htlcMaximumMsat.getOrElse(0 msat))) // select the largest edge + val hops = selectedEdges.map(d => Hop(d.desc.a, d.desc.b, d.update)) + sender ! RouteResponse(hops, Set.empty, Set.empty) + case _ => // some nodes in the supplied route aren't connected in our graph + sender ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) + } stay case Event(RouteRequest(start, end, amount, assistedRoutes, ignoreNodes, ignoreChannels, params_opt), d) => // 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 assistedUpdates = assistedRoutes.flatMap(toFakeUpdates(_, end)).toMap - // we also filter out updates corresponding to channels/nodes that are blacklisted for this particular request - // TODO: in case of duplicates, d.updates will be overridden by assistedUpdates even if they are more recent! - val ignoredUpdates = getIgnoredChannelDesc(d.updates ++ d.privateUpdates ++ assistedUpdates, ignoreNodes) ++ ignoreChannels ++ d.excludedChannels - val extraEdges = assistedUpdates.map { case (c, u) => GraphEdge(c, u) }.toSet + val assistedChannels: Map[ShortChannelId, AssistedChannel] = assistedRoutes.flatMap(toAssistedChannels(_, end, amount)).toMap + val extraEdges = assistedChannels.values.map(ac => GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum))).toSet + val ignoredEdges = ignoreChannels ++ d.excludedChannels val params = params_opt.getOrElse(defaultRouteParams) val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedUpdates.keys.mkString(","), ignoreNodes.map(_.value).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding a route $start->$end with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), ignoreNodes.map(_.value).mkString(","), ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params) - findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredUpdates.toSet, routeParams = params) + findRoute(d.graph, start, end, amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredEdges, ignoredVertices = ignoreNodes, routeParams = params, nodeParams.currentBlockHeight) .map(r => sender ! RouteResponse(r, ignoreNodes, ignoreChannels)) .recover { case t => sender ! Status.Failure(t) } stay - case Event(SendChannelQuery(remoteNodeId, remote), d) => + case Event(SendChannelQuery(remoteNodeId, remote, flags_opt), d) => // ask for everything // we currently send only one query_channel_range message per peer, when we just (re)connected to it, so we don't // have to worry about sending a new query_channel_range when another query is still in progress - val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) + val query = QueryChannelRange(nodeParams.chainHash, firstBlockNum = 0L, numberOfBlocks = Int.MaxValue.toLong, TlvStream(flags_opt.toList)) log.info("sending query_channel_range={}", query) remote ! query @@ -327,20 +382,8 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // will start a new complete sync process stay using d.copy(sync = d.sync - remoteNodeId) - case Event(SendChannelQueryEx(remoteNodeId, remote), d) => - // ask for everything - val query = QueryChannelRangeEx(nodeParams.chainHash, firstBlockNum = 0, numberOfBlocks = Int.MaxValue) - log.info("sending query_channel_range_ex={}", query) - remote ! query - // we also set a pass-all filter for now (we can update it later) - val filter = GossipTimestampFilter(nodeParams.chainHash, firstTimestamp = 0, timestampRange = Int.MaxValue) - remote ! filter - // clean our sync state for this peer: we receive a SendChannelQuery just when we connect/reconnect to a peer and - // will start a new complete sync process - stay using d.copy(sync = d.sync - remoteNodeId) - // Warning: order matters here, this must be the first match for HasChainHash messages ! - case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), d) if routingMessage.chainHash != nodeParams.chainHash => + case Event(PeerRoutingMessage(_, _, routingMessage: HasChainHash), _) if routingMessage.chainHash != nodeParams.chainHash => sender ! TransportHandler.ReadAck(routingMessage) log.warning("message {} for wrong chain {}, we're on {}", routingMessage, routingMessage.chainHash, nodeParams.chainHash) stay @@ -388,7 +431,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom // On Android, we don't validate announcements for now, it means that neither awaiting nor stashed announcements are used db.addChannel(c1, ByteVector32.Zeroes, Satoshi(0)) stay using d.copy( - channels = d.channels + (c1.shortChannelId -> c1), + channels = d.channels + (c1.shortChannelId -> PublicChannel(c1, ByteVector32.Zeroes, Satoshi(0), None, None)), privateChannels = d.privateChannels - c1.shortChannelId // we remove fake announcements that we may have made before) ) } @@ -401,199 +444,91 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom sender ! TransportHandler.ReadAck(n) stay // we just ignore node_announcements on Android - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, extendedQueryFlags_opt)), d) => sender ! TransportHandler.ReadAck(routingMessage) // On Android we ignore queries stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, _, _, _, shortChannelIds, _)), d) => sender ! TransportHandler.ReadAck(routingMessage) - // On Android we ignore queries - stay - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirShortChannelIds, useGzip) = ChannelRangeQueries.decodeShortChannelIds(data) - // keep our channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(keep(firstBlockNum, numberOfBlocks, _, d.channels, d.updates)) - val missing: SortedSet[ShortChannelId] = theirShortChannelIds -- ourShortChannelIds - log.info("received reply_channel_range, we're missing {} channel announcements/updates, format={} useGzip={}", missing.size, format, useGzip) - - val d1 = if (missing.nonEmpty) { - // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not - // send another query_short_channel_ids query if they're still processing one - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> Sync(sync.missing ++ missing, sync.totalMissingCount + missing.size))) - } - } else d - context.system.eventStream.publish(syncProgress(d1)) - - // we have channel announcement that they don't have: check if we can prune them - val pruningCandidates = { - val first = ShortChannelId(firstBlockNum.toInt, 0, 0) - val last = ShortChannelId((firstBlockNum + numberOfBlocks).toInt, 0xFFFFFFFF, 0xFFFF) - // channel ids are sorted so we can simplify our range check - val shortChannelIds = d.channels.keySet.dropWhile(_ < first).takeWhile(_ <= last) -- theirShortChannelIds - log.info("we have {} channel that they do not have", shortChannelIds.size) - d.channels.filterKeys(id => shortChannelIds.contains(id)) - } - - val staleChannels = getStaleChannels(pruningCandidates.values, d.updates) - val staleUpdates = staleChannels.map(d.channels).flatMap(c => Seq(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2), ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1))) - val channels1 = d.channels -- staleChannels - - // let's clean the db and send the events - db.removeChannels(staleChannels) // NB: this also removes channel updates - staleChannels.foreach { shortChannelId => - log.info("pruning shortChannelId={} (stale)", shortChannelId) - context.system.eventStream.publish(ChannelLost(shortChannelId)) - } - // we also need to remove updates from the graph - val staleChannelsToRemove = new mutable.MutableList[ChannelDesc] - staleChannels.map(d.channels).foreach( ca => { - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2) - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1) - }) - val graph1 = d.graph.removeEdges(staleChannelsToRemove) - stay using d1.copy(channels = channels1, updates = d.updates -- staleUpdates, graph = graph1) + Kamon.runWithContextEntry(remoteNodeIdKey, remoteNodeId.toString) { + Kamon.runWithSpan(Kamon.spanBuilder("reply-channel-range").start(), finishSpan = true) { + + @tailrec + def loop(ids: List[ShortChannelId], 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 => + val flag = computeFlag(d.channels)(head, timestamps.headOption, checksums.headOption, nodeParams.routerConf.requestNodeAnnouncements) + // 0 means nothing to query, just don't include it + val acc1 = if (flag != 0) ShortChannelIdAndFlag(head, flag) :: acc else acc + loop(tail, timestamps.drop(1), checksums.drop(1), acc1) + } + } - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks, _, data)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - val (format, theirTimestampMap) = ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(data) - val theirShortChannelIds = theirTimestampMap.keySet - // keep our ids that match [block, block + numberOfBlocks] - val ourShortChannelIds: SortedSet[ShortChannelId] = d.channels.keySet.filter(id => { - val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) - height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) - }) + val timestamps_opt = routingMessage.timestamps_opt.map(_.timestamps).getOrElse(List.empty[ReplyChannelRangeTlv.Timestamps]) + val checksums_opt = routingMessage.checksums_opt.map(_.checksums).getOrElse(List.empty[ReplyChannelRangeTlv.Checksums]) - // missing are the ones we don't have - val missing = theirShortChannelIds -- ourShortChannelIds + val shortChannelIdAndFlags = { + loop(shortChannelIds.array, timestamps_opt, checksums_opt) + } - // outdated are the ones for which our update timestamp is older that theirs - val outdated = ourShortChannelIds.filter(id => { - theirShortChannelIds.contains(id) && Router.getTimestamp(d.channels, d.updates)(id) < theirTimestampMap(id) - }) - log.info("received reply_channel_range_ex, we're missing {} channel announcements and we have {} outdated ones, format={} ", missing.size, outdated.size, format) - // we first sync missing channels, then outdated ones - val d1 = if (missing.nonEmpty) { - // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not - // send another query_short_channel_ids query if they're still processing one - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(rest, missing.size, outdated, outdated.size))) - case Some(sync) => - // we already have a pending query with this peer, add missing ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = sync.missing ++ missing, totalMissingCount = sync.totalMissingCount + missing.size))) - } - } else if (outdated.nonEmpty) { - d.sync.get(remoteNodeId) match { - case None => - // we don't have a pending query with this peer - val (slice, rest) = outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, format, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> Sync(SortedSet(), 0, outdated, outdated.size))) - case Some(sync) => - // we already have a pending query with this peer, add outdated ids to our "sync" state - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = sync.outdated ++ outdated, totalOutdatedCount = sync.totalOutdatedCount + outdated.size))) + val (channelCount, updatesCount) = shortChannelIdAndFlags.foldLeft((0, 0)) { + case ((c, u), ShortChannelIdAndFlag(_, flag)) => + val c1 = c + (if (QueryShortChannelIdsTlv.QueryFlagType.includeChannelAnnouncement(flag)) 1 else 0) + val u1 = u + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate1(flag)) 1 else 0) + (if (QueryShortChannelIdsTlv.QueryFlagType.includeUpdate2(flag)) 1 else 0) + (c1, u1) + } + log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding) + // we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time) + val replies = shortChannelIdAndFlags + .grouped(nodeParams.routerConf.channelQueryChunkSize) + .map(chunk => QueryShortChannelIds(chainHash, + shortChannelIds = EncodedShortChannelIds(shortChannelIds.encoding, chunk.map(_.shortChannelId)), + if (routingMessage.timestamps_opt.isDefined || routingMessage.checksums_opt.isDefined) + TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag))) + else + TlvStream.empty + )) + .toList + val (sync1, replynow_opt) = addToSync(d.sync, remoteNodeId, replies) + // we only send a reply right away if there were no pending requests + replynow_opt.foreach(transport ! _) + val progress = syncProgress(sync1) + context.system.eventStream.publish(progress) + self ! progress + stay using d.copy(sync = sync1) } - } else d - context.system.eventStream.publish(syncProgress(d1)) - - // we have channel announcement that they don't have: check if we can prune them - val pruningCandidates = { - val first = ShortChannelId(firstBlockNum.toInt, 0, 0) - val last = ShortChannelId((firstBlockNum + numberOfBlocks).toInt, 0xFFFFFFFF, 0xFFFF) - // channel ids are sorted so we can simplify our range check - val shortChannelIds = d.channels.keySet.dropWhile(_ < first).takeWhile(_ <= last) -- theirShortChannelIds - log.info("we have {} channel that they do not have", shortChannelIds.size) - d.channels.filterKeys(id => shortChannelIds.contains(id)) } - val staleChannels = getStaleChannels(pruningCandidates.values, d.updates) - val staleUpdates = staleChannels.map(d.channels).flatMap(c => Seq(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2), ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1))) - val channels1 = d.channels -- staleChannels - - // let's clean the db and send the events - db.removeChannels(staleChannels) // NB: this also removes channel updates - staleChannels.foreach { shortChannelId => - log.info("pruning shortChannelId={} (stale)", shortChannelId) - context.system.eventStream.publish(ChannelLost(shortChannelId)) - } - // we also need to remove updates from the graph - val staleChannelsToRemove = new mutable.MutableList[ChannelDesc] - staleChannels.map(d.channels).foreach( ca => { - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2) - staleChannelsToRemove += ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1) - }) - val graph1 = d.graph.removeEdges(staleChannelsToRemove) - - stay using d1.copy(channels = channels1, updates = d.updates -- staleUpdates, graph = graph1) - - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIds(chainHash, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@QueryShortChannelIds(chainHash, shortChannelIds, _)), d) => sender ! TransportHandler.ReadAck(routingMessage) // On Android we ignore queries stay - case Event(PeerRoutingMessage(transport, _, routingMessage@QueryShortChannelIdsEx(chainHash, flag, data)), d) => + case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage: ReplyShortChannelIdsEnd), d) => sender ! TransportHandler.ReadAck(routingMessage) - // On Android we ignore queries - stay - - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEnd(chainHash, complete)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end={}", routingMessage) // have we more channels to ask this peer? - val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.missing.nonEmpty => - log.info(s"asking {} for the next slice of short_channel_ids", remoteNodeId) - val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIds(chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) - case Some(sync) if sync.missing.isEmpty => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - d.copy(sync = d.sync - remoteNodeId) - case _ => - d + val sync1 = d.sync.get(remoteNodeId) match { + case Some(sync) => + sync.pending match { + case nextRequest +: rest => + log.info(s"asking for the next slice of short_channel_ids (remaining=${sync.pending.size}/${sync.total})") + transport ! nextRequest + d.sync + (remoteNodeId -> sync.copy(pending = rest)) + case Nil => + // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove + // the remote peer from our map + log.info(s"sync complete (total=${sync.total})") + d.sync - remoteNodeId + } + case _ => d.sync } - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 + val progress = syncProgress(sync1) + context.system.eventStream.publish(progress) + self ! progress + stay using d.copy(sync = sync1) - case Event(PeerRoutingMessage(transport, remoteNodeId, routingMessage@ReplyShortChannelIdsEndEx(chainHash, complete)), d) => - sender ! TransportHandler.ReadAck(routingMessage) - log.info("received reply_short_channel_ids_end_ex={}", routingMessage) - // have we more channels to ask this peer? - val d1 = d.sync.get(remoteNodeId) match { - case Some(sync) if sync.missing.nonEmpty => - log.info(s"asking {} for the next slice of missing short_channel_ids", remoteNodeId) - val (slice, rest) = sync.missing.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(missing = rest))) - case Some(sync) if sync.outdated.nonEmpty => - log.info(s"asking {} for the next slice of outdated short_channel_ids", remoteNodeId) - val (slice, rest) = sync.outdated.splitAt(SHORTID_WINDOW) - transport ! QueryShortChannelIdsEx(chainHash, 0.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(slice, ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(outdated = rest))) - case Some(sync) if sync.missing.isEmpty && sync.outdated.isEmpty => - // we received reply_short_channel_ids_end for our last query and have not sent another one, we can now remove - // the remote peer from our map - d.copy(sync = d.sync - remoteNodeId) - case _ => - d - } - context.system.eventStream.publish(syncProgress(d1)) - stay using d1 } initialize() @@ -615,7 +550,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom context.system.eventStream.publish(NodeUpdated(n)) db.updateNode(n) d.copy(nodes = d.nodes + (n.nodeId -> n)) - } else if (d.channels.values.exists(c => isRelatedTo(c, n.nodeId))) { + } else if (d.channels.values.exists(c => isRelatedTo(c.ann, n.nodeId))) { log.debug("added node nodeId={}", n.nodeId) context.system.eventStream.publish(NodesDiscovered(n :: Nil)) db.addNode(n) @@ -630,44 +565,43 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom d } - def handle(u: ChannelUpdate, origin: ActorRef, d: Data, remoteNodeId_opt: Option[PublicKey] = None, transport_opt: Option[ActorRef] = None): Data = { + def handle(uOriginal: ChannelUpdate, origin: ActorRef, d: Data, remoteNodeId_opt: Option[PublicKey] = None, transport_opt: Option[ActorRef] = None): Data = { // On Android, after checking the sig we remove as much data as possible to reduce RAM consumption - val u1 = u.copy( - signature = null, - chainHash = null - ) + require(uOriginal.chainHash == nodeParams.chainHash, s"invalid chainhash for $uOriginal, we're on ${nodeParams.chainHash}") + // instead of keeping a copy of chainhash in each channel_update we now have a reference to then same object + val u = uOriginal.copy(signature = null, chainHash = nodeParams.chainHash) 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 c = d.channels(u.shortChannelId) - val desc = getDesc(u, c) + val pc = d.channels(u.shortChannelId) + val desc = getDesc(u, pc.ann) if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (duplicate)", u) d - } else if (!Announcements.checkSig(u, desc.a)) { + } else if (!Announcements.checkSig(uOriginal, pc.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (d.updates.contains(desc)) { + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannelUpdate(u1) + db.updateChannel(u) // update the graph - val graph1 = Announcements.isEnabled(u1.channelFlags) match { - case true => d.graph.removeEdge(desc).addEdge(desc, u1) + val graph1 = Announcements.isEnabled(u.channelFlags) match { + case true => d.graph.removeEdge(desc).addEdge(desc, u) case false => d.graph.removeEdge(desc) // if the channel is now disabled, we remove it from the graph } - d.copy(updates = d.updates + (desc -> u1), graph = graph1) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.addChannelUpdate(u1) + db.updateChannel(u) // we also need to update the graph - val graph1 = d.graph.addEdge(desc, u1) - d.copy(updates = d.updates + (desc -> u1), privateUpdates = d.privateUpdates - desc, graph = graph1) + val graph1 = d.graph.addEdge(desc, u) + d.copy(channels = d.channels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), privateChannels = d.privateChannels - u.shortChannelId, graph = graph1) } } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { // channel is currently being validated @@ -681,32 +615,31 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } } else if (d.privateChannels.contains(u.shortChannelId)) { val publicChannel = false - val remoteNodeId = d.privateChannels(u.shortChannelId) - val (a, b) = if (Announcements.isNode1(nodeParams.nodeId, remoteNodeId)) (nodeParams.nodeId, remoteNodeId) else (remoteNodeId, nodeParams.nodeId) - val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, a, b) else ChannelDesc(u.shortChannelId, b, a) + val pc = d.privateChannels(u.shortChannelId) + val desc = if (Announcements.isNode1(u.channelFlags)) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) if (isStale(u)) { log.debug("ignoring {} (stale)", u) d - } else if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (already know same or newer)", u) d } else if (!Announcements.checkSig(u, desc.a)) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) origin ! InvalidSignature(u) d - } else if (d.privateUpdates.contains(desc)) { + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph - val graph1 = d.graph.removeEdge(desc).addEdge(desc, u1) - d.copy(privateUpdates = d.privateUpdates + (desc -> u1), graph = graph1) + val graph1 = d.graph.removeEdge(desc).addEdge(desc, u) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) context.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph - val graph1 = d.graph.addEdge(desc, u1) - d.copy(privateUpdates = d.privateUpdates + (desc -> u1), graph = graph1) - } + val graph1 = d.graph.addEdge(desc, u) + d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc.updateChannelUpdateSameSideAs(u)), graph = graph1) + } } else { // On android we don't track pruned channels in our db log.debug("ignoring announcement {} (unknown channel)", u) @@ -715,7 +648,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom } override def mdc(currentMessage: Any): MDC = currentMessage match { - case SendChannelQuery(remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) + case SendChannelQuery(remoteNodeId, _, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case PeerRoutingMessage(_, remoteNodeId, _) => Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) case _ => akka.event.Logging.emptyMDC } @@ -723,19 +656,30 @@ class Router(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Prom object Router { + val shortChannelIdKey = Context.key[ShortChannelId]("shortChannelId", ShortChannelId(0)) + val remoteNodeIdKey = Context.key[String]("remoteNodeId", "unknown") + def props(nodeParams: NodeParams, watcher: ActorRef, initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) - def toFakeUpdate(extraHop: ExtraHop): 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, Platform.currentTime.milliseconds.toSeconds, messageFlags = 0, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0L, extraHop.feeBaseMsat, extraHop.feeProportionalMillionths, None) + 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, Platform.currentTime.milliseconds.toSeconds, messageFlags = 1, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) + } - def toFakeUpdates(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Map[ChannelDesc, ChannelUpdate] = { + 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).map { - case (extraHop: ExtraHop, nextNodeId) => (ChannelDesc(extraHop.shortChannelId, extraHop.nodeId, nextNodeId) -> toFakeUpdate(extraHop)) - }.toMap + 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 getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = { @@ -745,106 +689,290 @@ object Router { def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 - def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelAnnouncement]): Boolean = channels.exists(c => isRelatedTo(c, nodeId)) + def hasChannels(nodeId: PublicKey, channels: Iterable[PublicChannel]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) + + def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp) - def isStale(u: ChannelUpdate): Boolean = { + def isStale(timestamp: Long): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks" // but we don't want to prune brand new channels for which we didn't yet receive a channel update val staleThresholdSeconds = (Platform.currentTime.milliseconds - 14.days).toSeconds - u.timestamp < staleThresholdSeconds + timestamp < staleThresholdSeconds + } + + def isAlmostStale(timestamp: Long): Boolean = { + // we define almost stale as 2 weeks minus 4 days + val staleThresholdSeconds = (Platform.currentTime.milliseconds - 10.days).toSeconds + timestamp < staleThresholdSeconds } /** - * Is stale a channel that: - * (1) is older than 2 weeks (2*7*144 = 2016 blocks) - * AND - * (2) has no channel_update younger than 2 weeks - * - * @param channel - * @param update1_opt update corresponding to one side of the channel, if we have it - * @param update2_opt update corresponding to the other side of the channel, if we have it - * @return - */ - def isStale(channel: ChannelAnnouncement, update1_opt: Option[ChannelUpdate], update2_opt: Option[ChannelUpdate]): Boolean = { + * Is stale a channel that: + * (1) is older than 2 weeks (2*7*144 = 2016 blocks) + * AND + * (2) has no channel_update younger than 2 weeks + * + * @param update1_opt update corresponding to one side of the channel, if we have it + * @param update2_opt update corresponding to the other side of the channel, if we have it + * @return + */ + def isStale(channel: ChannelAnnouncement, update1_opt: Option[ChannelUpdate], update2_opt: Option[ChannelUpdate], currentBlockHeight: Long): Boolean = { // BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)" // but we don't want to prune brand new channels for which we didn't yet receive a channel update, so we keep them as long as they are less than 2 weeks (2016 blocks) old - val staleThresholdBlocks = Globals.blockCount.get() - 2016 + val staleThresholdBlocks = currentBlockHeight - 2016 val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(channel.shortChannelId) - blockHeight < staleThresholdBlocks && update1_opt.map(isStale).getOrElse(true) && update2_opt.map(isStale).getOrElse(true) + blockHeight < staleThresholdBlocks && update1_opt.forall(isStale) && update2_opt.forall(isStale) } - def getStaleChannels(channels: Iterable[ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Iterable[ShortChannelId] = { - val staleChannels = channels.filter { c => - val update1 = updates.get(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) - val update2 = updates.get(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) - isStale(c, update1, update2) - } - staleChannels.map(_.shortChannelId) - } + def getStaleChannels(channels: Iterable[PublicChannel], currentBlockHeight: Long): Iterable[PublicChannel] = channels.filter(data => isStale(data.ann, data.update_1_opt, data.update_2_opt, currentBlockHeight)) /** - * Filters channels that we want to send to nodes asking for a channel range - */ - def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId, channels: Map[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Boolean = { + * Filters channels that we want to send to nodes asking for a channel range + */ + def keep(firstBlockNum: Long, numberOfBlocks: Long, id: ShortChannelId): Boolean = { val TxCoordinates(height, _, _) = ShortChannelId.coordinates(id) height >= firstBlockNum && height <= (firstBlockNum + numberOfBlocks) } - def syncProgress(d: Data): SyncProgress = - if (d.sync.isEmpty) { + def shouldRequestUpdate(ourTimestamp: Long, ourChecksum: Long, theirTimestamp_opt: Option[Long], theirChecksum_opt: Option[Long]): Boolean = { + (theirTimestamp_opt, theirChecksum_opt) match { + case (Some(theirTimestamp), Some(theirChecksum)) => + // we request their channel_update if all those conditions are met: + // - it is more recent than ours + // - it is different from ours, or it is the same but ours is about to be stale + // - it is not stale + val theirsIsMoreRecent = ourTimestamp < theirTimestamp + val areDifferent = ourChecksum != theirChecksum + val oursIsAlmostStale = isAlmostStale(ourTimestamp) + val theirsIsStale = isStale(theirTimestamp) + theirsIsMoreRecent && (areDifferent || oursIsAlmostStale) && !theirsIsStale + case (Some(theirTimestamp), None) => + // if we only have their timestamp, we request their channel_update if theirs is more recent than ours + val theirsIsMoreRecent = ourTimestamp < theirTimestamp + val theirsIsStale = isStale(theirTimestamp) + theirsIsMoreRecent && !theirsIsStale + case (None, Some(theirChecksum)) => + // if we only have their checksum, we request their channel_update if it is different from ours + // NB: a zero checksum means that they don't have the data + val areDifferent = theirChecksum != 0 && ourChecksum != theirChecksum + areDifferent + case (None, None) => + // if we have neither their timestamp nor their checksum we request their channel_update + true + } + } + + def computeFlag(channels: SortedMap[ShortChannelId, PublicChannel])( + shortChannelId: ShortChannelId, + theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], + theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums], + includeNodeAnnouncements: Boolean): Long = { + import QueryShortChannelIdsTlv.QueryFlagType._ + + val flagsNodes = if (includeNodeAnnouncements) INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2 else 0 + + val flags = if (!channels.contains(shortChannelId)) { + INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 + } else { + // we already know this channel + val (ourTimestamps, ourChecksums) = Router.getChannelDigestInfo(channels)(shortChannelId) + // if they don't provide timestamps or checksums, we set appropriate default values: + // - we assume their timestamp is more recent than ours by setting timestamp = Long.MaxValue + // - we assume their update is different from ours by setting checkum = Long.MaxValue (NB: our default value for checksum is 0) + val shouldRequestUpdate1 = shouldRequestUpdate(ourTimestamps.timestamp1, ourChecksums.checksum1, theirTimestamps_opt.map(_.timestamp1), theirChecksums_opt.map(_.checksum1)) + val shouldRequestUpdate2 = shouldRequestUpdate(ourTimestamps.timestamp2, ourChecksums.checksum2, theirTimestamps_opt.map(_.timestamp2), theirChecksums_opt.map(_.checksum2)) + val flagUpdate1 = if (shouldRequestUpdate1) INCLUDE_CHANNEL_UPDATE_1 else 0 + val flagUpdate2 = if (shouldRequestUpdate2) INCLUDE_CHANNEL_UPDATE_2 else 0 + flagUpdate1 | flagUpdate2 + } + + if (flags == 0) 0 else flags | flagsNodes + } + + /** + * Handle a query message, which includes a list of channel ids and flags. + * + * @param nodes node id -> node announcement + * @param channels channel id -> channel announcement + updates + * @param ids list of channel ids + * @param flags list of query flags, either empty one flag per channel id + * @param onChannel called when a channel announcement matches (i.e. its bit is set in the query flag and we have it) + * @param onUpdate called when a channel update matches + * @param onNode called when a node announcement matches + * + */ + def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], + channels: SortedMap[ShortChannelId, PublicChannel])( + ids: List[ShortChannelId], + flags: List[Long], + onChannel: ChannelAnnouncement => Unit, + onUpdate: ChannelUpdate => Unit, + onNode: NodeAnnouncement => Unit)(implicit log: LoggingAdapter): Unit = { + import QueryShortChannelIdsTlv.QueryFlagType + + // 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 { + 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) + loop(tail, flags.drop(1), numca, numcu, nodesSent) + case head :: tail => + val numca1 = numca + val numcu1 = numcu + var sent1 = nodesSent + val pc = channels(head) + val flag_opt = flags.headOption + // no flag means send everything + + val includeChannel = flag_opt.forall(QueryFlagType.includeChannelAnnouncement) + val includeUpdate1 = flag_opt.forall(QueryFlagType.includeUpdate1) + val includeUpdate2 = flag_opt.forall(QueryFlagType.includeUpdate2) + val includeNode1 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement1) + val includeNode2 = flag_opt.forall(QueryFlagType.includeNodeAnnouncement2) + + if (includeChannel) { + onChannel(pc.ann) + } + if (includeUpdate1) { + pc.update_1_opt.foreach { u => + onUpdate(u) + } + } + if (includeUpdate2) { + pc.update_2_opt.foreach { u => + onUpdate(u) + } + } + if (includeNode1 && !sent1.contains(pc.ann.nodeId1)) { + nodes.get(pc.ann.nodeId1).foreach { n => + onNode(n) + sent1 = sent1 + pc.ann.nodeId1 + } + } + if (includeNode2 && !sent1.contains(pc.ann.nodeId2)) { + nodes.get(pc.ann.nodeId2).foreach { n => + onNode(n) + sent1 = sent1 + pc.ann.nodeId2 + } + } + loop(tail, flags.drop(1), numca1, numcu1, sent1) + } + + loop(ids, flags) + } + + /** + * Returns overall progress on synchronization + * + * @return a sync progress indicator (1 means fully synced) + */ + def syncProgress(sync: Map[PublicKey, Sync]): SyncProgress = { + // NB: progress is in terms of requests, not individual channels + val (pending, total) = sync.foldLeft((0, 0)) { + case ((p, t), (_, sync)) => (p + sync.pending.size, t + sync.total) + } + if (total == 0) { SyncProgress(1) } else { - SyncProgress(1 - d.sync.values.map(v => v.missing.size + v.outdated.size).sum * 1.0 / d.sync.values.map(v => v.totalMissingCount + v.totalOutdatedCount).sum) + SyncProgress((total - pending) / (1.0 * total)) } + } /** - * This method is used after a payment failed, and we want to exclude some nodes that we know are failing - */ - def getIgnoredChannelDesc(updates: Map[ChannelDesc, ChannelUpdate], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { + * 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) { Iterable.empty[ChannelDesc] } else { // expensive, but node blacklisting shouldn't happen often - updates.keys.filter(desc => ignoreNodes.contains(desc.a) || ignoreNodes.contains(desc.b)) + channels.values + .filter(channelData => ignoreNodes.contains(channelData.ann.nodeId1) || ignoreNodes.contains(channelData.ann.nodeId2)) + .flatMap(channelData => Vector(ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId1, channelData.ann.nodeId2), ChannelDesc(channelData.ann.shortChannelId, channelData.ann.nodeId2, channelData.ann.nodeId1))) } desc } + def getChannelDigestInfo(channels: SortedMap[ShortChannelId, PublicChannel])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { + val c = channels(shortChannelId) + val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L) + val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L) + val checksum1 = c.update_1_opt.map(getChecksum).getOrElse(0L) + val checksum2 = c.update_2_opt.map(getChecksum).getOrElse(0L) + (ReplyChannelRangeTlv.Timestamps(timestamp1 = timestamp1, timestamp2 = timestamp2), ReplyChannelRangeTlv.Checksums(checksum1 = checksum1, checksum2 = checksum2)) + } + + def crc32c(data: ByteVector): Long = { + import com.google.common.hash.Hashing + Hashing.crc32c().hashBytes(data.toArray).asInt() & 0xFFFFFFFFL + } + + def getChecksum(u: ChannelUpdate): Long = { + import u._ + + val data = serializationResult(LightningMessageCodecs.channelUpdateChecksumCodec.encode(chainHash :: shortChannelId :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: HNil)) + crc32c(data) + } + + case class ShortChannelIdsChunk(firstBlock: Long, numBlocks: Long, shortChannelIds: List[ShortChannelId]) + /** - * - * @param channels id -> announcement map - * @param updates channel updates - * @param id short channel id - * @return the timestamp of the most recent update for this channel id, 0 if we don't have any - */ - def getTimestamp(channels: SortedMap[ShortChannelId, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate])(id: ShortChannelId): Long = { - val ca = channels(id) - val opt1 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId1, ca.nodeId2)) - val opt2 = updates.get(ChannelDesc(ca.shortChannelId, ca.nodeId2, ca.nodeId1)) - val timestamp = (opt1, opt2) match { - case (Some(u1), Some(u2)) => Math.max(u1.timestamp, u2.timestamp) - case (Some(u1), None) => u1.timestamp - case (None, Some(u2)) => u2.timestamp - case (None, None) => - 0L + * Have to split ids because otherwise message could be too big + * there could be several reply_channel_range messages for a single query + */ + def split(shortChannelIds: SortedSet[ShortChannelId], channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { + // this algorithm can split blocks (meaning that we can in theory generate several replies with the same first_block/num_blocks + // and a different set of short_channel_ids) but it doesn't matter + if (shortChannelIds.isEmpty) { + List(ShortChannelIdsChunk(0, 0, List.empty)) + } else { + shortChannelIds + .grouped(channelRangeChunkSize) + .toList + .map { group => + // NB: group is never empty + val firstBlock: Long = ShortChannelId.coordinates(group.head).blockHeight.toLong + val numBlocks: Long = ShortChannelId.coordinates(group.last).blockHeight.toLong - firstBlock + 1 + ShortChannelIdsChunk(firstBlock, numBlocks, group.toList) + } + } + } + + def addToSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = { + pending match { + case head +: rest => + // they may send back several reply_channel_range messages for a single query_channel_range query, and we must not + // send another query_short_channel_ids query if they're still processing one + syncMap.get(remoteNodeId) match { + case None => + // we don't have a pending query with this peer, let's send it + (syncMap + (remoteNodeId -> Sync(rest, pending.size)), Some(head)) + case Some(sync) => + // we already have a pending query with this peer, add missing ids to our "sync" state + (syncMap + (remoteNodeId -> Sync(sync.pending ++ pending, sync.total + pending.size)), None) + } + case Nil => + // there is nothing to send + (syncMap, None) } - timestamp } /** - * https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications - */ + * https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications + */ val ROUTE_MAX_LENGTH = 20 // Max allowed CLTV for a route - val DEFAULT_ROUTE_MAX_CLTV = 1008 + val DEFAULT_ROUTE_MAX_CLTV = CltvExpiryDelta(1008) - // The default amount of routes we'll search for when findRoute is called + // The default number of routes we'll search for when findRoute is called with randomize = true val DEFAULT_ROUTES_COUNT = 3 def getDefaultRouteParams(routerConf: RouterConf) = RouteParams( randomize = routerConf.randomizeRouteSelection, - maxFeeBaseMsat = routerConf.searchMaxFeeBaseSat * 1000, // converting sat -> msat + maxFeeBase = routerConf.searchMaxFeeBase.toMilliSatoshi, maxFeePct = routerConf.searchMaxFeePct, routeMaxLength = routerConf.searchMaxRouteLength, routeMaxCltv = routerConf.searchMaxCltv, @@ -859,41 +987,52 @@ object Router { ) /** - * Find a route in the graph between localNodeId and targetNodeId, returns the route. - * Will perform a k-shortest path selection given the @param numRoutes and randomly select one of the result. - * - * @param g - * @param localNodeId - * @param targetNodeId - * @param amountMsat the amount that will be sent along this route - * @param numRoutes the number of shortest-paths to find - * @param extraEdges a set of extra edges we want to CONSIDER during the search - * @param ignoredEdges a set of extra edges we want to IGNORE during the search - * @param routeParams a set of parameters that can restrict the route search - * @return the computed route to the destination @targetNodeId - */ + * Find a route in the graph between localNodeId and targetNodeId, returns the route. + * Will perform a k-shortest path selection given the @param numRoutes and randomly select one of the result. + * + * @param g graph of the whole network + * @param localNodeId sender node (payer) + * @param targetNodeId target node (final recipient) + * @param amount the amount that will be sent along this route + * @param numRoutes the number of shortest-paths to find + * @param extraEdges a set of extra edges we want to CONSIDER during the search + * @param ignoredEdges a set of extra edges we want to IGNORE during the search + * @param routeParams a set of parameters that can restrict the route search + * @return the computed route to the destination @targetNodeId + */ def findRoute(g: DirectedGraph, localNodeId: PublicKey, targetNodeId: PublicKey, - amountMsat: Long, + amount: MilliSatoshi, numRoutes: Int, extraEdges: Set[GraphEdge] = Set.empty, ignoredEdges: Set[ChannelDesc] = Set.empty, - routeParams: RouteParams): Try[Seq[Hop]] = Try { + ignoredVertices: Set[PublicKey] = Set.empty, + routeParams: RouteParams, + currentBlockHeight: Long): Try[Seq[Hop]] = Try { if (localNodeId == targetNodeId) throw CannotRouteToSelf - val currentBlockHeight = Globals.blockCount.get() + def feeBaseOk(fee: MilliSatoshi): Boolean = fee <= routeParams.maxFeeBase + + def feePctOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = { + val maxFee = amount * routeParams.maxFeePct + fee <= maxFee + } + + def feeOk(fee: MilliSatoshi, amount: MilliSatoshi): Boolean = feeBaseOk(fee) || feePctOk(fee, amount) + + def lengthOk(length: Int): Boolean = length <= routeParams.routeMaxLength && length <= ROUTE_MAX_LENGTH + + def cltvOk(cltv: CltvExpiryDelta): Boolean = cltv <= routeParams.routeMaxCltv val boundaries: RichWeight => Boolean = { weight => - ((weight.cost - amountMsat) < routeParams.maxFeeBaseMsat || (weight.cost - amountMsat) < (routeParams.maxFeePct * amountMsat)) && - weight.length <= routeParams.routeMaxLength && weight.length <= ROUTE_MAX_LENGTH && - weight.cltv <= routeParams.routeMaxCltv + feeOk(weight.cost - amount, amount) && lengthOk(weight.length) && cltvOk(weight.cltv) } - val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amountMsat, ignoredEdges, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { + val foundRoutes = Graph.yenKshortestPaths(g, localNodeId, targetNodeId, amount, ignoredEdges, ignoredVertices, extraEdges, numRoutes, routeParams.ratios, currentBlockHeight, boundaries).toList match { case Nil if routeParams.routeMaxLength < ROUTE_MAX_LENGTH => // if not found within the constraints we relax and repeat the search - return findRoute(g, localNodeId, targetNodeId, amountMsat, numRoutes, extraEdges, ignoredEdges, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV)) + return findRoute(g, localNodeId, targetNodeId, amount, numRoutes, extraEdges, ignoredEdges, ignoredVertices, routeParams.copy(routeMaxLength = ROUTE_MAX_LENGTH, routeMaxCltv = DEFAULT_ROUTE_MAX_CLTV), currentBlockHeight) case Nil => throw RouteNotFound case routes => routes.find(_.path.size == 1) match { case Some(directRoute) => directRoute :: Nil @@ -902,6 +1041,7 @@ object Router { } // At this point 'foundRoutes' cannot be empty - Random.shuffle(foundRoutes).head.path.map(graphEdgeToHop) + val randomizedRoutes = if (routeParams.randomize) Random.shuffle(foundRoutes) else foundRoutes + randomizedRoutes.head.path.map(graphEdgeToHop) } } 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 3448ecfe99..d4b201202d 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,6 +16,7 @@ package fr.acinq.eclair.transactions +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.wire._ /** @@ -30,8 +31,8 @@ case object OUT extends Direction { def opposite = IN } case class DirectedHtlc(direction: Direction, add: UpdateAddHtlc) -final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocalMsat: Long, toRemoteMsat: Long) { - val totalFunds = toLocalMsat + toRemoteMsat + htlcs.toSeq.map(_.add.amountMsat).sum +final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: Long, toLocal: MilliSatoshi, toRemote: MilliSatoshi) { + val totalFunds = toLocal + toRemote + htlcs.toSeq.map(_.add.amountMsat).sum } object CommitmentSpec { @@ -43,16 +44,16 @@ object CommitmentSpec { def addHtlc(spec: CommitmentSpec, direction: Direction, update: UpdateAddHtlc): CommitmentSpec = { val htlc = DirectedHtlc(direction, update) direction match { - case OUT => spec.copy(toLocalMsat = spec.toLocalMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) - case IN => spec.copy(toRemoteMsat = spec.toRemoteMsat - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) + case OUT => spec.copy(toLocal = spec.toLocal - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) + case IN => spec.copy(toRemote = spec.toRemote - htlc.add.amountMsat, htlcs = spec.htlcs + htlc) } } // OUT means we are sending an UpdateFulfillHtlc message which means that we are fulfilling an HTLC that they sent def fulfillHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match { - case Some(htlc) if direction == OUT => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == OUT => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == IN => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}") } } @@ -60,8 +61,8 @@ object CommitmentSpec { // OUT means we are sending an UpdateFailHtlc message which means that we are failing an HTLC that they sent def failHtlc(spec: CommitmentSpec, direction: Direction, htlcId: Long): CommitmentSpec = { spec.htlcs.find(htlc => htlc.direction != direction && htlc.add.id == htlcId) match { - case Some(htlc) if direction == OUT => spec.copy(toRemoteMsat = spec.toRemoteMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) - case Some(htlc) if direction == IN => spec.copy(toLocalMsat = spec.toLocalMsat + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == OUT => spec.copy(toRemote = spec.toRemote + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) + case Some(htlc) if direction == IN => spec.copy(toLocal = spec.toLocal + htlc.add.amountMsat, htlcs = spec.htlcs - htlc) case None => throw new RuntimeException(s"cannot find htlc id=${htlcId}") } } 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 41b5ca3a47..4afa3195e9 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,14 +16,15 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160} +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering, LockTimeThreshold, OP_0, OP_1, OP_1NEGATE, OP_2, OP_2DROP, OP_ADD, OP_CHECKLOCKTIMEVERIFY, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_NOTIF, OP_PUSHDATA, OP_SIZE, OP_SWAP, Satoshi, Script, ScriptElt, ScriptWitness, Transaction, TxIn} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering, LockTimeThreshold, OP_0, OP_1, OP_1NEGATE, OP_2, OP_CHECKLOCKTIMEVERIFY, OP_CHECKMULTISIG, OP_CHECKSEQUENCEVERIFY, OP_CHECKSIG, OP_DROP, OP_DUP, OP_ELSE, OP_ENDIF, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_IF, OP_NOTIF, OP_PUSHDATA, OP_SIZE, OP_SWAP, Satoshi, Script, ScriptElt, ScriptWitness, Transaction, TxIn} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount} import scodec.bits.ByteVector /** - * Created by PM on 02/12/2016. - */ + * Created by PM on 02/12/2016. + */ object Scripts { def der(sig: ByteVector64): ByteVector = Crypto.compact2der(sig) :+ 1 @@ -34,13 +35,13 @@ object Scripts { Script.createMultiSigMofN(2, Seq(pubkey2, pubkey1)) /** - * - * @param sig1 - * @param sig2 - * @param pubkey1 - * @param pubkey2 - * @return a script witness that matches the msig 2-of-2 pubkey script for pubkey1 and pubkey2 - */ + * + * @param sig1 + * @param sig2 + * @param pubkey1 + * @param pubkey2 + * @return a script witness that matches the msig 2-of-2 pubkey script for pubkey1 and pubkey2 + */ def witness2of2(sig1: ByteVector64, sig2: ByteVector64, pubkey1: PublicKey, pubkey2: PublicKey): ScriptWitness = { if (LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value)) ScriptWitness(Seq(ByteVector.empty, der(sig1), der(sig2), write(multiSig2of2(pubkey1, pubkey2)))) @@ -50,13 +51,13 @@ object Scripts { } /** - * minimal encoding of a number into a script element: - * - OP_0 to OP_16 if 0 <= n <= 16 - * - OP_PUSHDATA(encodeNumber(n)) otherwise - * - * @param n input number - * @return a script element that represents n - */ + * minimal encoding of a number into a script element: + * - OP_0 to OP_16 if 0 <= n <= 16 + * - OP_PUSHDATA(encodeNumber(n)) otherwise + * + * @param n input number + * @return a script element that represents n + */ def encodeNumber(n: Long): ScriptElt = n match { case 0 => OP_0 case -1 => OP_1NEGATE @@ -66,21 +67,21 @@ object Scripts { def applyFees(amount_us: Satoshi, amount_them: Satoshi, fee: Satoshi) = { val (amount_us1: Satoshi, amount_them1: Satoshi) = (amount_us, amount_them) match { - case (Satoshi(us), Satoshi(them)) if us >= fee.toLong / 2 && them >= fee.toLong / 2 => (Satoshi(us - fee.toLong / 2), Satoshi(them - fee.toLong / 2)) - case (Satoshi(us), Satoshi(them)) if us < fee.toLong / 2 => (Satoshi(0L), Satoshi(Math.max(0L, them - fee.toLong + us))) - case (Satoshi(us), Satoshi(them)) if them < fee.toLong / 2 => (Satoshi(Math.max(us - fee.toLong + them, 0L)), Satoshi(0L)) + case (us, them) if us >= fee / 2 && them >= fee / 2 => ((us - fee) / 2, (them - fee) / 2) + case (us, them) if us < fee / 2 => (0 sat, (them - fee + us).max(0 sat)) + case (us, them) if them < fee / 2 => ((us - fee + them).max(0 sat), 0 sat) } (amount_us1, amount_them1) } /** - * This function interprets the locktime for the given transaction, and returns the block height before which this tx cannot be published. - * By convention in bitcoin, depending of the value of locktime it might be a number of blocks or a number of seconds since epoch. - * This function does not support the case when the locktime is a number of seconds that is not way in the past. - * NB: We use this property in lightning to store data in this field. - * - * @return the block height before which this tx cannot be published. - */ + * This function interprets the locktime for the given transaction, and returns the block height before which this tx cannot be published. + * By convention in bitcoin, depending of the value of locktime it might be a number of blocks or a number of seconds since epoch. + * This function does not support the case when the locktime is a number of seconds that is not way in the past. + * NB: We use this property in lightning to store data in this field. + * + * @return the block height before which this tx cannot be published. + */ def cltvTimeout(tx: Transaction): Long = { if (tx.lockTime <= LockTimeThreshold) { // locktime is a number of blocks @@ -95,10 +96,10 @@ object Scripts { } /** - * - * @param tx - * @return the number of confirmations of the tx parent before which it can be published - */ + * + * @param tx + * @return the number of confirmations of the tx parent before which it can be published + */ def csvTimeout(tx: Transaction): Long = { def sequenceToBlockHeight(sequence: Long): Long = { if ((sequence & TxIn.SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) 0 @@ -112,12 +113,12 @@ object Scripts { else tx.txIn.map(_.sequence).map(sequenceToBlockHeight).max } - def toLocalDelayed(revocationPubkey: PublicKey, toSelfDelay: Int, localDelayedPaymentPubkey: PublicKey) = { + def toLocalDelayed(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey) = { // @formatter:off OP_IF :: OP_PUSHDATA(revocationPubkey) :: OP_ELSE :: - encodeNumber(toSelfDelay) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: + encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: OP_DROP :: OP_PUSHDATA(localDelayedPaymentPubkey) :: OP_ENDIF :: OP_CHECKSIG :: Nil @@ -125,15 +126,15 @@ object Scripts { } /** - * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay - */ + * This witness script spends a [[toLocalDelayed]] output using a local sig after a delay + */ def witnessToLocalDelayedAfterDelay(localSig: ByteVector64, toLocalDelayedScript: ByteVector) = ScriptWitness(der(localSig) :: ByteVector.empty :: toLocalDelayedScript :: Nil) /** - * This witness script spends (steals) a [[toLocalDelayed]] output using a revocation key as a punishment - * for having published a revoked transaction - */ + * This witness script spends (steals) a [[toLocalDelayed]] output using a revocation key as a punishment + * for having published a revoked transaction + */ def witnessToLocalDelayedWithRevocationSig(revocationSig: ByteVector64, toLocalScript: ByteVector) = ScriptWitness(der(revocationSig) :: ByteVector(1) :: toLocalScript :: Nil) @@ -157,19 +158,19 @@ object Scripts { } /** - * This is the witness script of the 2nd-stage HTLC Success transaction (consumes htlcOffered script from commit tx) - */ + * This is the witness script of the 2nd-stage HTLC Success transaction (consumes htlcOffered script from commit tx) + */ def witnessHtlcSuccess(localSig: ByteVector64, remoteSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector) = ScriptWitness(ByteVector.empty :: der(remoteSig) :: der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil) /** - * If local publishes its commit tx where there was a local->remote htlc, then remote uses this script to - * claim its funds using a payment preimage (consumes htlcOffered script from commit tx) - */ + * If local publishes its commit tx where there was a local->remote htlc, then remote uses this script to + * claim its funds using a payment preimage (consumes htlcOffered script from commit tx) + */ def witnessClaimHtlcSuccessFromCommitTx(localSig: ByteVector64, paymentPreimage: ByteVector32, htlcOfferedScript: ByteVector) = ScriptWitness(der(localSig) :: paymentPreimage.bytes :: htlcOfferedScript :: Nil) - def htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, lockTime: Long) = { + def htlcReceived(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, revocationPubKey: PublicKey, paymentHash: ByteVector, lockTime: CltvExpiry) = { // @formatter:off // To you with revocation key OP_DUP :: OP_HASH160 :: OP_PUSHDATA(revocationPubKey.hash160) :: OP_EQUAL :: @@ -183,7 +184,7 @@ object Scripts { OP_2 :: OP_SWAP :: OP_PUSHDATA(localHtlcPubkey) :: OP_2 :: OP_CHECKMULTISIG :: OP_ELSE :: // To you after timeout. - OP_DROP :: encodeNumber(lockTime) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP :: + OP_DROP :: encodeNumber(lockTime.toLong) :: OP_CHECKLOCKTIMEVERIFY :: OP_DROP :: OP_CHECKSIG :: OP_ENDIF :: OP_ENDIF :: Nil @@ -191,22 +192,22 @@ object Scripts { } /** - * This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcReceived script from commit tx) - */ + * This is the witness script of the 2nd-stage HTLC Timeout transaction (consumes htlcReceived script from commit tx) + */ def witnessHtlcTimeout(localSig: ByteVector64, remoteSig: ByteVector64, htlcReceivedScript: ByteVector) = ScriptWitness(ByteVector.empty :: der(remoteSig) :: der(localSig) :: ByteVector.empty :: htlcReceivedScript :: Nil) /** - * If local publishes its commit tx where there was a remote->local htlc, then remote uses this script to - * claim its funds after timeout (consumes htlcReceived script from commit tx) - */ + * If local publishes its commit tx where there was a remote->local htlc, then remote uses this script to + * claim its funds after timeout (consumes htlcReceived script from commit tx) + */ def witnessClaimHtlcTimeoutFromCommitTx(localSig: ByteVector64, htlcReceivedScript: ByteVector) = ScriptWitness(der(localSig) :: ByteVector.empty :: htlcReceivedScript :: Nil) /** - * This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment - * for having published a revoked transaction - */ + * This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment + * for having published a revoked transaction + */ def witnessHtlcWithRevocationSig(revocationSig: ByteVector64, revocationPubkey: PublicKey, htlcScript: ByteVector) = ScriptWitness(der(revocationSig) :: revocationPubkey.value :: htlcScript :: Nil) 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 0febeaab9e..1ba742a336 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 @@ -21,7 +21,8 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering, MilliSatoshi, OutPoint, Protocol, SIGHASH_ALL, Satoshi, Script, ScriptElt, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut, millisatoshi2satoshi} +import fr.acinq.bitcoin._ +import fr.acinq.eclair._ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.wire.UpdateAddHtlc import scodec.bits.ByteVector @@ -29,8 +30,8 @@ import scodec.bits.ByteVector import scala.util.Try /** - * Created by PM on 15/12/2016. - */ + * Created by PM on 15/12/2016. + */ object Transactions { // @formatter:off @@ -68,38 +69,39 @@ object Transactions { // @formatter:on /** - * When *local* *current* [[CommitTx]] is published: - * - [[ClaimDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay - * - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage - * - [[ClaimDelayedOutputTx]] spends [[HtlcSuccessTx]] after a delay - * - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout - * - [[ClaimDelayedOutputTx]] spends [[HtlcTimeoutTx]] after a delay - * - * When *remote* *current* [[CommitTx]] is published: - * - [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] - * - [[ClaimHtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage - * - [[ClaimHtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout - * - * When *remote* *revoked* [[CommitTx]] is published: - * - [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] - * - [[MainPenaltyTx]] spends remote main output using the per-commitment secret - * - [[HtlcSuccessTx]] spends htlc-sent outputs of [[CommitTx]] for which they have the preimage (published by remote) - * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local) - * - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by remote) - * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) - * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) - */ + * When *local* *current* [[CommitTx]] is published: + * - [[ClaimDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay + * - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage + * - [[ClaimDelayedOutputTx]] spends [[HtlcSuccessTx]] after a delay + * - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout + * - [[ClaimDelayedOutputTx]] spends [[HtlcTimeoutTx]] after a delay + * + * When *remote* *current* [[CommitTx]] is published: + * - [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] + * - [[ClaimHtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage + * - [[ClaimHtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout + * + * When *remote* *revoked* [[CommitTx]] is published: + * - [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] + * - [[MainPenaltyTx]] spends remote main output using the per-commitment secret + * - [[HtlcSuccessTx]] spends htlc-sent outputs of [[CommitTx]] for which they have the preimage (published by remote) + * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local) + * - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by remote) + * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) + * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) + */ /** - * these values are defined in the RFC - */ + * these values are defined in the RFC + */ val commitWeight = 724 + val htlcOutputWeight = 172 val htlcTimeoutWeight = 663 val htlcSuccessWeight = 703 /** - * these values specific to us and used to estimate fees - */ + * these values specific to us and used to estimate fees + */ val claimP2WPKHOutputWeight = 438 val claimHtlcDelayedWeight = 483 val claimHtlcSuccessWeight = 571 @@ -110,44 +112,53 @@ object Transactions { def weight2fee(feeratePerKw: Long, weight: Int) = Satoshi((feeratePerKw * weight) / 1000) /** - * - * @param fee tx fee - * @param weight tx weight - * @return the fee rate (in Satoshi/Kw) for this tx - */ - def fee2rate(fee: Satoshi, weight: Int) = (fee.amount * 1000L) / weight + * + * @param fee tx fee + * @param weight tx weight + * @return the fee rate (in Satoshi/Kw) for this tx + */ + def fee2rate(fee: Satoshi, weight: Int) = (fee.toLong * 1000L) / weight + + /** Offered HTLCs below this amount will be trimmed. */ + def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feeratePerKw, htlcTimeoutWeight) def trimOfferedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { - val htlcTimeoutFee = weight2fee(spec.feeratePerKw, htlcTimeoutWeight) + val threshold = offeredHtlcTrimThreshold(dustLimit, spec) spec.htlcs .filter(_.direction == OUT) - .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcTimeoutFee)) + .filter(htlc => htlc.add.amountMsat >= threshold) .toSeq } + /** Received HTLCs below this amount will be trimmed. */ + def receivedHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feeratePerKw, htlcSuccessWeight) + def trimReceivedHtlcs(dustLimit: Satoshi, spec: CommitmentSpec): Seq[DirectedHtlc] = { - val htlcSuccessFee = weight2fee(spec.feeratePerKw, htlcSuccessWeight) + val threshold = receivedHtlcTrimThreshold(dustLimit, spec) spec.htlcs .filter(_.direction == IN) - .filter(htlc => MilliSatoshi(htlc.add.amountMsat) >= (dustLimit + htlcSuccessFee)) + .filter(htlc => htlc.add.amountMsat >= threshold) .toSeq } + /** Fee for an un-trimmed HTLC. */ + def htlcOutputFee(feeratePerKw: Long): Satoshi = weight2fee(feeratePerKw, htlcOutputWeight) + def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = { val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec) val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec) - val weight = commitWeight + 172 * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) + val weight = commitWeight + htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size) weight2fee(spec.feeratePerKw, weight) } /** - * - * @param commitTxNumber commit tx number - * @param isFunder true if local node is funder - * @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) - */ + * + * @param commitTxNumber commit tx number + * @param isFunder true if local node is funder + * @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 = { // from BOLT 3: SHA256(payment-basepoint from open_channel || payment-basepoint from accept_channel) val h = if (isFunder) @@ -160,13 +171,13 @@ object Transactions { } /** - * - * @param commitTx commit tx - * @param isFunder true if local node is funder - * @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 - */ + * + * @param commitTx commit tx + * @param isFunder true if local node is funder + * @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 = { val blind = obscuredCommitTxNumber(0, isFunder, localPaymentBasePoint, remotePaymentBasePoint) val obscured = decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) @@ -174,11 +185,11 @@ object Transactions { } /** - * This is a trick to split and encode a 48-bit txnumber into the sequence and locktime fields of a tx - * - * @param txnumber commitment number - * @return (sequence, locktime) - */ + * This is a trick to split and encode a 48-bit txnumber into the sequence and locktime fields of a tx + * + * @param txnumber commitment number + * @return (sequence, locktime) + */ def encodeTxNumber(txnumber: Long): (Long, Long) = { require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") (0x80000000L | (txnumber >> 24), (txnumber & 0xffffffL) | 0x20000000) @@ -186,22 +197,22 @@ object Transactions { def decodeTxNumber(sequence: Long, locktime: Long): Long = ((sequence & 0xffffffL) << 24) + (locktime & 0xffffffL) - def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): CommitTx = { + def makeCommitTx(commitTxInput: InputInfo, commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, localIsFunder: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, remotePaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): CommitTx = { val commitFee = commitTxFee(localDustLimit, spec) val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - commitFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + (spec.toLocal.truncateToSatoshi - commitFee, spec.toRemote.truncateToSatoshi) } else { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - commitFee) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway val toLocalDelayedOutput_opt = if (toLocalAmount >= localDustLimit) Some(TxOut(toLocalAmount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))) else None val toRemoteOutput_opt = if (toRemoteAmount >= localDustLimit) Some(TxOut(toRemoteAmount, pay2wpkh(remotePaymentPubkey))) else None val htlcOfferedOutputs = trimOfferedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) + .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes))))) val htlcReceivedOutputs = trimReceivedHtlcs(localDustLimit, spec) - .map(htlc => TxOut(MilliSatoshi(htlc.add.amountMsat), pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) + .map(htlc => TxOut(htlc.add.amountMsat.truncateToSatoshi, pay2wsh(htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.add.paymentHash.bytes), htlc.add.cltvExpiry)))) val txnumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, locktime) = encodeTxNumber(txnumber) @@ -214,12 +225,12 @@ object Transactions { CommitTx(commitTxInput, LexicographicalOrdering.sort(tx)) } - def makeHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcTimeoutTx = { + def makeHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcTimeoutTx = { val fee = weight2fee(feeratePerKw, htlcTimeoutWeight) val redeemScript = htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) - val amount = MilliSatoshi(htlc.amountMsat) - fee + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } @@ -228,15 +239,15 @@ object Transactions { version = 2, txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil, - lockTime = htlc.cltvExpiry)) + lockTime = htlc.cltvExpiry.toLong)) } - def makeHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcSuccessTx = { + def makeHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, feeratePerKw: Long, htlc: UpdateAddHtlc): HtlcSuccessTx = { val fee = weight2fee(feeratePerKw, htlcSuccessWeight) val redeemScript = htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) - val amount = MilliSatoshi(htlc.amountMsat) - fee + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) + val amount = htlc.amountMsat.truncateToSatoshi - fee if (amount < localDustLimit) { throw AmountBelowDustLimit } @@ -248,7 +259,7 @@ object Transactions { lockTime = 0), htlc.paymentHash) } - def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { + def makeHtlcTxs(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, spec: CommitmentSpec): (Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { var outputsAlreadyUsed = Set.empty[Int] // this is needed to handle cases where we have several identical htlcs val htlcTimeoutTxs = trimOfferedHtlcs(localDustLimit, spec).map { htlc => val htlcTx = makeHtlcTimeoutTx(commitTx, outputsAlreadyUsed, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec.feeratePerKw, htlc.add) @@ -266,7 +277,7 @@ object Transactions { def makeClaimHtlcSuccessTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcSuccessTx = { val redeemScript = htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes)) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) val tx = Transaction( @@ -289,7 +300,7 @@ object Transactions { def makeClaimHtlcTimeoutTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], localDustLimit: Satoshi, localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, htlc: UpdateAddHtlc, feeratePerKw: Long): ClaimHtlcTimeoutTx = { val redeemScript = htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlc.paymentHash.bytes), htlc.cltvExpiry) val pubkeyScript = write(pay2wsh(redeemScript)) - val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(Satoshi(htlc.amountMsat / 1000))) + val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = Some(htlc.amountMsat.truncateToSatoshi)) val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript)) // unsigned tx @@ -297,7 +308,7 @@ object Transactions { version = 2, txIn = TxIn(input.outPoint, ByteVector.empty, 0x00000000L) :: Nil, txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, - lockTime = htlc.cltvExpiry) + lockTime = htlc.cltvExpiry.toLong) val weight = addSigs(ClaimHtlcTimeoutTx(input, tx), PlaceHolderSig).tx.weight() val fee = weight2fee(feeratePerKw, weight) @@ -337,7 +348,7 @@ object Transactions { ClaimP2WPKHOutputTx(input, tx1) } - def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputTx = { + def makeClaimDelayedOutputTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputTx = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) @@ -346,7 +357,7 @@ object Transactions { // unsigned transaction val tx = Transaction( version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay) :: Nil, + txIn = TxIn(input.outPoint, ByteVector.empty, toLocalDelay.toInt) :: Nil, txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, lockTime = 0) @@ -363,7 +374,7 @@ object Transactions { ClaimDelayedOutputTx(input, tx1) } - def makeClaimDelayedOutputPenaltyTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputPenaltyTx = { + def makeClaimDelayedOutputPenaltyTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): ClaimDelayedOutputPenaltyTx = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) @@ -389,7 +400,7 @@ object Transactions { ClaimDelayedOutputPenaltyTx(input, tx1) } - def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: Int, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = { + def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = { val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) @@ -416,8 +427,8 @@ object Transactions { } /** - * We already have the redeemScript, no need to build it - */ + * We already have the redeemScript, no need to build it + */ def makeHtlcPenaltyTx(commitTx: Transaction, outputsAlreadyUsed: Set[Int], redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: Long): HtlcPenaltyTx = { val pubkeyScript = write(pay2wsh(redeemScript)) val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript, outputsAlreadyUsed, amount_opt = None) @@ -447,9 +458,9 @@ object Transactions { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)) - closingFee, millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat))) + (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) } else { - (millisatoshi2satoshi(MilliSatoshi(spec.toLocalMsat)), millisatoshi2satoshi(MilliSatoshi(spec.toRemoteMsat)) - closingFee) + (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) } // NB: we don't care if values are < 0, they will be trimmed if they are < dust limit anyway val toLocalOutput_opt = if (toLocalAmount >= dustLimit) Some(TxOut(toLocalAmount, localScriptPubKey)) else None @@ -466,7 +477,7 @@ object Transactions { def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector, outputsAlreadyUsed: Set[Int], amount_opt: Option[Satoshi]): Int = { val outputIndex = tx.txOut .zipWithIndex - .indexWhere { case (txOut, index) => amount_opt.map(_ == txOut.amount).getOrElse(true) && txOut.publicKeyScript == pubkeyScript && !outputsAlreadyUsed.contains(index)} // it's not enough to only resolve on pubkeyScript because we may have duplicates + .indexWhere { case (txOut, index) => amount_opt.map(_ == txOut.amount).getOrElse(true) && txOut.publicKeyScript == pubkeyScript && !outputsAlreadyUsed.contains(index) } // it's not enough to only resolve on pubkeyScript because we may have duplicates if (outputIndex >= 0) { outputIndex } else { @@ -475,14 +486,14 @@ object Transactions { } /** - * Default public key used for fee estimation - */ + * Default public key used for fee estimation + */ val PlaceHolderPubKey = PrivateKey(ByteVector32.One).publicKey /** - * This default sig takes 72B when encoded in DER (incl. 1B for the trailing sig hash), it is used for fee estimation - * It is 72 bytes because our signatures are normalized (low-s) and will take up 72 bytes at most in DER format - */ + * This default sig takes 72B when encoded in DER (incl. 1B for the trailing sig hash), it is used for fee estimation + * It is 72 bytes because our signatures are normalized (low-s) and will take up 72 bytes at most in DER format + */ val PlaceHolderSig = ByteVector64(ByteVector.fill(64)(0xaa)) assert(der(PlaceHolderSig).size == 72) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 97937c05de..258ca597da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -55,17 +55,18 @@ object ChannelCodecs extends Logging { .typecase(0x01, bits(ChannelVersion.LENGTH_BITS).as[ChannelVersion]) // NB: 0x02 and 0x03 are *reserved* for backward compatibility reasons , - fallback = provide(ChannelVersion.STANDARD) + fallback = provide(ChannelVersion.ZEROES) // README: DO NOT CHANGE THIS !! old channels don't have a channel version + // field and don't support additional features which is why all bits are set to 0. ) val localParamsCodec: Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: - ("toSelfDelay" | uint16) :: + ("channelReserve" | satoshi) :: + ("htlcMinimum" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: @@ -74,11 +75,11 @@ object ChannelCodecs extends Logging { val remoteParamsCodec: Codec[RemoteParams] = ( ("nodeId" | publicKey) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: - ("toSelfDelay" | uint16) :: + ("channelReserve" | satoshi) :: + ("htlcMinimum" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: ("fundingPubKey" | publicKey) :: ("revocationBasepoint" | publicKey) :: @@ -105,8 +106,8 @@ object ChannelCodecs extends Logging { val commitmentSpecCodec: Codec[CommitmentSpec] = ( ("htlcs" | setCodec(htlcCodec)) :: ("feeratePerKw" | uint32) :: - ("toLocalMsat" | uint64overflow) :: - ("toRemoteMsat" | uint64overflow)).as[CommitmentSpec] + ("toLocal" | millisatoshi) :: + ("toRemote" | millisatoshi)).as[CommitmentSpec] val outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => OutPoint.write(d))) @@ -186,8 +187,8 @@ object ChannelCodecs extends Logging { val relayedCodec: Codec[Relayed] = ( ("originChannelId" | bytes32) :: ("originHtlcId" | int64) :: - ("amountMsatIn" | uint64overflow) :: - ("amountMsatOut" | uint64overflow)).as[Relayed] + ("amountIn" | millisatoshi) :: + ("amountOut" | millisatoshi)).as[Relayed] // this is for backward compatibility to handle legacy payments that didn't have identifiers val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index e56a3645fd..24456b9bba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -31,14 +31,14 @@ import scala.Ordering.Implicits._ import scala.util.Try /** - * Created by t-bast on 20/06/2019. - */ + * Created by t-bast on 20/06/2019. + */ object CommonCodecs { /** - * Discriminator codec with a default fallback codec (of the same type). - */ + * Discriminator codec with a default fallback codec (of the same type). + */ def discriminatorWithDefault[A](discriminator: Codec[A], fallback: Codec[A]): Codec[A] = new Codec[A] { def sizeBound: SizeBound = discriminator.sizeBound | fallback.sizeBound @@ -52,18 +52,27 @@ object CommonCodecs { // this codec can be safely used for values < 2^63 and will fail otherwise // (for something smarter see https://github.com/yzernik/bitcoin-scodec/blob/master/src/main/scala/io/github/yzernik/bitcoinscodec/structures/UInt64.scala) val uint64overflow: Codec[Long] = int64.narrow(l => if (l >= 0) Attempt.Successful(l) else Attempt.failure(Err(s"overflow for value $l")), l => l) - val uint64: Codec[UInt64] = bytes(8).xmap(b => UInt64(b), a => a.toByteVector.padLeft(8)) + val satoshi: Codec[Satoshi] = uint64overflow.xmapc(l => Satoshi(l))(_.toLong) + val millisatoshi: Codec[MilliSatoshi] = uint64overflow.xmapc(l => MilliSatoshi(l))(_.toLong) + + val cltvExpiry: Codec[CltvExpiry] = uint32.xmapc(CltvExpiry)((_: CltvExpiry).toLong) + val cltvExpiryDelta: Codec[CltvExpiryDelta] = uint16.xmapc(CltvExpiryDelta)((_: CltvExpiryDelta).toInt) + + // this is needed because some millisatoshi values are encoded on 32 bits in the BOLTs + // this codec will fail if the amount does not fit on 32 bits + val millisatoshi32: Codec[MilliSatoshi] = uint32.xmapc(l => MilliSatoshi(l))(_.toLong) + /** - * We impose a minimal encoding on some values (such as varint and truncated int) to ensure that signed hashes can be - * re-computed correctly. - * If a value could be encoded with less bytes, it's considered invalid and results in a failed decoding attempt. - * - * @param codec the value codec (depends on the value). - * @param min the minimal value that should be encoded. - */ - def minimalvalue[A : Ordering](codec: Codec[A], min: A): Codec[A] = codec.exmap({ + * We impose a minimal encoding on some values (such as varint and truncated int) to ensure that signed hashes can be + * re-computed correctly. + * If a value could be encoded with less bytes, it's considered invalid and results in a failed decoding attempt. + * + * @param codec the value codec (depends on the value). + * @param min the minimal value that should be encoded. + */ + def minimalvalue[A: Ordering](codec: Codec[A], min: A): Codec[A] = codec.exmap({ case i if i < min => Attempt.failure(Err("value was not minimally encoded")) case i => Attempt.successful(i) }, Attempt.successful) @@ -127,9 +136,9 @@ object CommonCodecs { def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) /** - * When encoding, prepend a valid mac to the output of the given codec. - * When decoding, verify that a valid mac is prepended. - */ + * When encoding, prepend a valid mac to the output of the given codec. + * When decoding, verify that a valid mac is prepended. + */ def prependmac[A](codec: Codec[A], mac: Mac32) = Codec[A]( (a: A) => codec.encode(a).map(bits => mac.mac(bits.toByteVector).bits ++ bits), (bits: BitVector) => ("mac" | bytes32).decode(bits) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index a7cee716dd..442545bfa3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -18,18 +18,25 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} +import fr.acinq.eclair.wire.CommonCodecs._ +import fr.acinq.eclair.wire.FailureMessageCodecs.failureMessageCodec import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, MilliSatoshi, UInt64} import scodec.codecs._ import scodec.{Attempt, Codec} /** - * see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md - * Created by fabrice on 14/03/17. - */ + * see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md + * Created by fabrice on 14/03/17. + */ // @formatter:off -sealed trait FailureMessage { def message: String } +sealed trait FailureMessage { + def message: String + // We actually encode the failure message, which is a bit clunky and not particularly efficient. + // It would be nice to be able to get that value from the discriminated codec directly. + lazy val code: Int = failureMessageCodec.encode(this).flatMap(uint16.decode).require.value +} sealed trait BadOnion extends FailureMessage { def onionHash: ByteVector32 } sealed trait Perm extends FailureMessage sealed trait Node extends FailureMessage @@ -42,22 +49,34 @@ case object RequiredNodeFeatureMissing extends Perm with Node { def message = "p case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } -case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion per-hop payload could not be parsed" } case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the route" } -case class AmountBelowMinimum(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } -case class FeeInsufficient(amountMsat: Long, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } +case class AmountBelowMinimum(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" } +case class FeeInsufficient(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" } case class ChannelDisabled(messageFlags: Byte, channelFlags: Byte, update: ChannelUpdate) extends Update { def message = "channel is currently disabled" } -case class IncorrectCltvExpiry(expiry: Long, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" } -case class IncorrectOrUnknownPaymentDetails(amountMsat: Long) extends Perm { def message = "incorrect payment amount or unknown payment hash" } -case object IncorrectPaymentAmount extends Perm { def message = "payment amount is incorrect" } +case class IncorrectCltvExpiry(expiry: CltvExpiry, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" } +case class IncorrectOrUnknownPaymentDetails(amount: MilliSatoshi, height: Long) extends Perm { def message = "incorrect payment details or unknown payment hash" } case class ExpiryTooSoon(update: ChannelUpdate) extends Update { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" } -case object FinalExpiryTooSoon extends FailureMessage { def message = "payment expiry is too close to the current block height for safe handling by the final node" } -case class FinalIncorrectCltvExpiry(expiry: Long) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } -case class FinalIncorrectHtlcAmount(amountMsat: Long) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } +case class FinalIncorrectCltvExpiry(expiry: CltvExpiry) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } +case class FinalIncorrectHtlcAmount(amount: MilliSatoshi) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" } +case class InvalidOnionPayload(tag: UInt64, offset: Int) extends Perm { def message = "onion per-hop payload is invalid" } + +/** + * We allow remote nodes to send us unknown failure codes (e.g. deprecated failure codes). + * By reading the PERM and NODE bits we can still extract useful information for payment retry even without knowing how + * to decode the failure payload (but we can't extract a channel update or onion hash). + */ +sealed trait UnknownFailureMessage extends FailureMessage { + def message = "unknown failure message" + override def toString = s"$message (${code.toHexString})" + override def equals(obj: Any): Boolean = obj match { + case f: UnknownFailureMessage => f.code == code + case _ => false + } +} // @formatter:on object FailureMessageCodecs { @@ -72,44 +91,51 @@ object FailureMessageCodecs { // this codec supports both versions for decoding, and will encode with the message type val channelUpdateWithLengthCodec = variableSizeBytes(uint16, choice(channelUpdateCodecWithType, channelUpdateCodec)) - val failureMessageCodec = discriminated[FailureMessage].by(uint16) - .typecase(PERM | 1, provide(InvalidRealm)) - .typecase(NODE | 2, provide(TemporaryNodeFailure)) - .typecase(PERM | 2, provide(PermanentNodeFailure)) - .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) - .typecase(BADONION | PERM, sha256.as[InvalidOnionPayload]) - .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) - .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) - .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) - .typecase(UPDATE | 7, ("channelUpdate" | channelUpdateWithLengthCodec).as[TemporaryChannelFailure]) - .typecase(PERM | 8, provide(PermanentChannelFailure)) - .typecase(PERM | 9, provide(RequiredChannelFeatureMissing)) - .typecase(PERM | 10, provide(UnknownNextPeer)) - .typecase(UPDATE | 11, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum]) - .typecase(UPDATE | 12, (("amountMsat" | uint64overflow) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) - .typecase(UPDATE | 13, (("expiry" | uint32) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) - .typecase(UPDATE | 14, ("channelUpdate" | channelUpdateWithLengthCodec).as[ExpiryTooSoon]) - .typecase(UPDATE | 20, (("messageFlags" | byte) :: ("channelFlags" | byte) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) - .typecase(PERM | 15, ("amountMsat" | withDefaultValue(optional(bitsRemaining, uint64overflow), 0L)).as[IncorrectOrUnknownPaymentDetails]) - .typecase(PERM | 16, provide(IncorrectPaymentAmount)) - .typecase(17, provide(FinalExpiryTooSoon)) - .typecase(18, ("expiry" | uint32).as[FinalIncorrectCltvExpiry]) - .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount]) - .typecase(21, provide(ExpiryTooFar)) - - /** - * Return the failure code for a given failure message. This method actually encodes the failure message, which is a - * bit clunky and not particularly efficient. It shouldn't be used on the application's hot path. - */ - def failureCode(failure: FailureMessage): Int = failureMessageCodec.encode(failure).flatMap(uint16.decode).require.value + val failureMessageCodec = discriminatorWithDefault( + discriminated[FailureMessage].by(uint16) + .typecase(PERM | 1, provide(InvalidRealm)) + .typecase(NODE | 2, provide(TemporaryNodeFailure)) + .typecase(PERM | NODE | 2, provide(PermanentNodeFailure)) + .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) + .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) + .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) + .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) + .typecase(UPDATE | 7, ("channelUpdate" | channelUpdateWithLengthCodec).as[TemporaryChannelFailure]) + .typecase(PERM | 8, provide(PermanentChannelFailure)) + .typecase(PERM | 9, provide(RequiredChannelFeatureMissing)) + .typecase(PERM | 10, provide(UnknownNextPeer)) + .typecase(UPDATE | 11, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[AmountBelowMinimum]) + .typecase(UPDATE | 12, (("amountMsat" | millisatoshi) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[FeeInsufficient]) + .typecase(UPDATE | 13, (("expiry" | cltvExpiry) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[IncorrectCltvExpiry]) + .typecase(UPDATE | 14, ("channelUpdate" | channelUpdateWithLengthCodec).as[ExpiryTooSoon]) + .typecase(UPDATE | 20, (("messageFlags" | byte) :: ("channelFlags" | byte) :: ("channelUpdate" | channelUpdateWithLengthCodec)).as[ChannelDisabled]) + .typecase(PERM | 15, (("amountMsat" | withDefaultValue(optional(bitsRemaining, millisatoshi), 0 msat)) :: ("height" | withDefaultValue(optional(bitsRemaining, uint32), 0L))).as[IncorrectOrUnknownPaymentDetails]) + // PERM | 16 (incorrect_payment_amount) has been deprecated because it allowed probing attacks: IncorrectOrUnknownPaymentDetails should be used instead. + // PERM | 17 (final_expiry_too_soon) has been deprecated because it allowed probing attacks: IncorrectOrUnknownPaymentDetails should be used instead. + .typecase(18, ("expiry" | cltvExpiry).as[FinalIncorrectCltvExpiry]) + .typecase(19, ("amountMsat" | millisatoshi).as[FinalIncorrectHtlcAmount]) + .typecase(21, provide(ExpiryTooFar)) + .typecase(PERM | 22, (("tag" | varint) :: ("offset" | uint16)).as[InvalidOnionPayload]), + uint16.xmap(code => { + val failureMessage = code match { + // @formatter:off + case fc if (fc & PERM) != 0 && (fc & NODE) != 0 => new UnknownFailureMessage with Perm with Node { override lazy val code = fc } + case fc if (fc & NODE) != 0 => new UnknownFailureMessage with Node { override lazy val code = fc } + case fc if (fc & PERM) != 0 => new UnknownFailureMessage with Perm { override lazy val code = fc } + case fc => new UnknownFailureMessage { override lazy val code = fc } + // @formatter:on + } + failureMessage.asInstanceOf[FailureMessage] + }, (_: FailureMessage).code) + ) /** - * 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 - */ + * 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( paddedFixedSizeBytesDependent( 260, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index b7e3891c43..5c65700c51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -22,8 +22,8 @@ import scodec.Codec import scodec.codecs._ /** - * Created by PM on 15/11/2016. - */ + * Created by PM on 15/11/2016. + */ object LightningMessageCodecs { val initCodec: Codec[Init] = ( @@ -51,14 +51,14 @@ object LightningMessageCodecs { val openChannelCodec: Codec[OpenChannel] = ( ("chainHash" | bytes32) :: ("temporaryChannelId" | bytes32) :: - ("fundingSatoshis" | uint64overflow) :: - ("pushMsat" | uint64overflow) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("fundingSatoshis" | satoshi) :: + ("pushMsat" | millisatoshi) :: + ("dustLimitSatoshis" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserveSatoshis" | satoshi) :: + ("htlcMinimumMsat" | millisatoshi) :: ("feeratePerKw" | uint32) :: - ("toSelfDelay" | uint16) :: + ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: ("fundingPubkey" | publicKey) :: ("revocationBasepoint" | publicKey) :: @@ -70,12 +70,12 @@ object LightningMessageCodecs { val acceptChannelCodec: Codec[AcceptChannel] = ( ("temporaryChannelId" | bytes32) :: - ("dustLimitSatoshis" | uint64overflow) :: + ("dustLimitSatoshis" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserveSatoshis" | uint64overflow) :: - ("htlcMinimumMsat" | uint64overflow) :: + ("channelReserveSatoshis" | satoshi) :: + ("htlcMinimumMsat" | millisatoshi) :: ("minimumDepth" | uint32) :: - ("toSelfDelay" | uint16) :: + ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: ("fundingPubkey" | publicKey) :: ("revocationBasepoint" | publicKey) :: @@ -104,15 +104,15 @@ object LightningMessageCodecs { val closingSignedCodec: Codec[ClosingSigned] = ( ("channelId" | bytes32) :: - ("feeSatoshis" | uint64overflow) :: + ("feeSatoshis" | satoshi) :: ("signature" | bytes64)).as[ClosingSigned] val updateAddHtlcCodec: Codec[UpdateAddHtlc] = ( ("channelId" | bytes32) :: ("id" | uint64overflow) :: - ("amountMsat" | uint64overflow) :: + ("amountMsat" | millisatoshi) :: ("paymentHash" | bytes32) :: - ("expiry" | uint32) :: + ("expiry" | cltvExpiry) :: ("onionRoutingPacket" | OnionCodecs.paymentOnionPacketCodec)).as[UpdateAddHtlc] val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( @@ -182,17 +182,29 @@ object LightningMessageCodecs { ("signature" | bytes64) :: nodeAnnouncementWitnessCodec).as[NodeAnnouncement] + val channelUpdateChecksumCodec = + ("chainHash" | bytes32) :: + ("shortChannelId" | shortchannelid) :: + (("messageFlags" | byte) >>:~ { messageFlags => + ("channelFlags" | byte) :: + ("cltvExpiryDelta" | cltvExpiryDelta) :: + ("htlcMinimumMsat" | millisatoshi) :: + ("feeBaseMsat" | millisatoshi32) :: + ("feeProportionalMillionths" | uint32) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) + }) + val channelUpdateWitnessCodec = ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: ("timestamp" | uint32) :: (("messageFlags" | byte) >>:~ { messageFlags => ("channelFlags" | byte) :: - ("cltvExpiryDelta" | uint16) :: - ("htlcMinimumMsat" | uint64overflow) :: - ("feeBaseMsat" | uint32) :: + ("cltvExpiryDelta" | cltvExpiryDelta) :: + ("htlcMinimumMsat" | millisatoshi) :: + ("feeBaseMsat" | millisatoshi32) :: ("feeProportionalMillionths" | uint32) :: - ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, uint64overflow)) :: + ("htlcMaximumMsat" | conditional((messageFlags & 1) != 0, millisatoshi)) :: ("unknownFields" | bytes) }) @@ -200,29 +212,43 @@ object LightningMessageCodecs { ("signature" | bytes64) :: channelUpdateWitnessCodec).as[ChannelUpdate] - val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( - ("chainHash" | bytes32) :: - ("data" | varsizebinarydata) + val encodedShortChannelIdsCodec: Codec[EncodedShortChannelIds] = + discriminated[EncodedShortChannelIds].by(byte) + .\(0) { case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + + val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = { + Codec( + ("chainHash" | bytes32) :: + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("tlvStream" | QueryShortChannelIdsTlv.codec) ).as[QueryShortChannelIds] + } val replyShortChanelIdsEndCodec: Codec[ReplyShortChannelIdsEnd] = ( ("chainHash" | bytes32) :: ("complete" | byte) ).as[ReplyShortChannelIdsEnd] - val queryChannelRangeCodec: Codec[QueryChannelRange] = ( - ("chainHash" | bytes32) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) - ).as[QueryChannelRange] - - val replyChannelRangeCodec: Codec[ReplyChannelRange] = ( - ("chainHash" | bytes32) :: - ("firstBlockNum" | uint32) :: - ("numberOfBlocks" | uint32) :: - ("complete" | byte) :: - ("data" | varsizebinarydata) - ).as[ReplyChannelRange] + val queryChannelRangeCodec: Codec[QueryChannelRange] = { + Codec( + ("chainHash" | bytes32) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("tlvStream" | QueryChannelRangeTlv.codec) + ).as[QueryChannelRange] + } + + val replyChannelRangeCodec: Codec[ReplyChannelRange] = { + Codec( + ("chainHash" | bytes32) :: + ("firstBlockNum" | uint32) :: + ("numberOfBlocks" | uint32) :: + ("complete" | byte) :: + ("shortChannelIds" | variableSizeBytes(uint16, encodedShortChannelIdsCodec)) :: + ("tlvStream" | ReplyChannelRangeTlv.codec) + ).as[ReplyChannelRange] + } val gossipTimestampFilterCodec: Codec[GossipTimestampFilter] = ( ("chainHash" | bytes32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index c1e6aa17e2..e2aa4a90d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -21,15 +21,16 @@ import java.nio.charset.StandardCharsets import com.google.common.base.Charsets import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.eclair.{ShortChannelId, UInt64} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector import scala.util.Try /** - * Created by PM on 15/11/2016. - */ + * Created by PM on 15/11/2016. + */ // @formatter:off sealed trait LightningMessage @@ -68,14 +69,14 @@ case class ChannelReestablish(channelId: ByteVector32, case class OpenChannel(chainHash: ByteVector32, temporaryChannelId: ByteVector32, - fundingSatoshis: Long, - pushMsat: Long, - dustLimitSatoshis: Long, - maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + fundingSatoshis: Satoshi, + pushMsat: MilliSatoshi, + dustLimitSatoshis: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + channelReserveSatoshis: Satoshi, + htlcMinimumMsat: MilliSatoshi, feeratePerKw: Long, - toSelfDelay: Int, + toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, fundingPubkey: PublicKey, revocationBasepoint: PublicKey, @@ -86,12 +87,12 @@ case class OpenChannel(chainHash: ByteVector32, channelFlags: Byte) extends ChannelMessage with HasTemporaryChannelId with HasChainHash case class AcceptChannel(temporaryChannelId: ByteVector32, - dustLimitSatoshis: Long, - maxHtlcValueInFlightMsat: UInt64, - channelReserveSatoshis: Long, - htlcMinimumMsat: Long, + dustLimitSatoshis: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + channelReserveSatoshis: Satoshi, + htlcMinimumMsat: MilliSatoshi, minimumDepth: Long, - toSelfDelay: Int, + toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, fundingPubkey: PublicKey, revocationBasepoint: PublicKey, @@ -115,14 +116,14 @@ case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector) extends ChannelMessage with HasChannelId case class ClosingSigned(channelId: ByteVector32, - feeSatoshis: Long, + feeSatoshis: Satoshi, signature: ByteVector64) extends ChannelMessage with HasChannelId case class UpdateAddHtlc(channelId: ByteVector32, id: Long, - amountMsat: Long, + amountMsat: MilliSatoshi, paymentHash: ByteVector32, - cltvExpiry: Long, + cltvExpiry: CltvExpiry, onionRoutingPacket: OnionRoutingPacket) extends HtlcMessage with UpdateMessage with HasChannelId case class UpdateFulfillHtlc(channelId: ByteVector32, @@ -216,74 +217,70 @@ case class ChannelUpdate(signature: ByteVector64, timestamp: Long, messageFlags: Byte, channelFlags: Byte, - cltvExpiryDelta: Int, - htlcMinimumMsat: Long, - feeBaseMsat: Long, + cltvExpiryDelta: CltvExpiryDelta, + htlcMinimumMsat: MilliSatoshi, + feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, - htlcMaximumMsat: Option[Long], + htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector = ByteVector.empty) extends RoutingMessage with HasTimestamp with HasChainHash { require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") + + def isNode1 = Announcements.isNode1(channelFlags) } -/** - * - * @param chainHash chain hash - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ +// @formatter:off +sealed trait EncodingType +object EncodingType { + case object UNCOMPRESSED extends EncodingType + case object COMPRESSED_ZLIB extends EncodingType +} +// @formatter:on + +case class EncodedShortChannelIds(encoding: EncodingType, + array: List[ShortChannelId]) + case class QueryShortChannelIds(chainHash: ByteVector32, - data: ByteVector) extends RoutingMessage with HasChainHash + shortChannelIds: EncodedShortChannelIds, + tlvStream: TlvStream[QueryShortChannelIdsTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash { + val queryFlags_opt: Option[QueryShortChannelIdsTlv.EncodedQueryFlags] = tlvStream.get[QueryShortChannelIdsTlv.EncodedQueryFlags] +} + +case class ReplyShortChannelIdsEnd(chainHash: ByteVector32, + complete: Byte) extends RoutingMessage with HasChainHash -/** - * - * @param chainHash chain hash - * @param flag if flag == 1, don't send back channel announcements - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ -case class QueryShortChannelIdsEx(chainHash: ByteVector32, - flag: Byte, - data: ByteVector) extends RoutingMessage with HasChainHash case class QueryChannelRange(chainHash: ByteVector32, firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash - -case class QueryChannelRangeEx(chainHash: ByteVector32, - firstBlockNum: Long, - numberOfBlocks: Long) extends RoutingMessage with HasChainHash + numberOfBlocks: Long, + tlvStream: TlvStream[QueryChannelRangeTlv] = TlvStream.empty) extends RoutingMessage { + val queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags] = tlvStream.get[QueryChannelRangeTlv.QueryFlags] +} -/** - * - * @param chainHash chain hash - * @param firstBlockNum first block that is found in data - * @param numberOfBlocks number of blocks spanned by data - * @param complete - * @param data prefix + list of short channel ids, where prefix specifies how the list is encoded - */ case class ReplyChannelRange(chainHash: ByteVector32, firstBlockNum: Long, numberOfBlocks: Long, complete: Byte, - data: ByteVector) extends RoutingMessage with HasChainHash + shortChannelIds: EncodedShortChannelIds, + tlvStream: TlvStream[ReplyChannelRangeTlv] = TlvStream.empty) extends RoutingMessage { + val timestamps_opt: Option[ReplyChannelRangeTlv.EncodedTimestamps] = tlvStream.get[ReplyChannelRangeTlv.EncodedTimestamps] -/** - * - * @param chainHash chain hash - * @param firstBlockNum first block that is found in data - * @param numberOfBlocks number of blocks spanned by data - * @param complete - * @param data prefix + list of (short channel id + timestamp) values, where prefix specifies how the list is encoded - */ -case class ReplyChannelRangeEx(chainHash: ByteVector32, - firstBlockNum: Long, - numberOfBlocks: Long, - complete: Byte, - data: ByteVector) extends RoutingMessage with HasChainHash + val checksums_opt: Option[ReplyChannelRangeTlv.EncodedChecksums] = tlvStream.get[ReplyChannelRangeTlv.EncodedChecksums] +} -case class ReplyShortChannelIdsEnd(chainHash: ByteVector32, - complete: Byte) extends RoutingMessage with HasChainHash +object ReplyChannelRange { + def apply(chainHash: ByteVector32, + firstBlockNum: Long, + numberOfBlocks: Long, + complete: Byte, + shortChannelIds: EncodedShortChannelIds, + timestamps: Option[ReplyChannelRangeTlv.EncodedTimestamps], + checksums: Option[ReplyChannelRangeTlv.EncodedChecksums]) = { + timestamps.foreach(ts => require(ts.timestamps.length == shortChannelIds.array.length)) + checksums.foreach(cs => require(cs.checksums.length == shortChannelIds.array.length)) + new ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, complete, shortChannelIds, TlvStream(timestamps.toList ::: checksums.toList)) + } +} -case class ReplyShortChannelIdsEndEx(chainHash: ByteVector32, - complete: Byte) extends RoutingMessage with HasChainHash case class GossipTimestampFilter(chainHash: ByteVector32, firstTimestamp: Long, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala index 32d942f547..d1ff115951 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -17,49 +17,163 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.crypto.Sphinx -import scodec.bits.{BitVector, ByteVector} -import scodec.codecs._ -import scodec.{Codec, DecodeResult, Decoder} +import fr.acinq.eclair.wire.CommonCodecs._ +import fr.acinq.eclair.wire.TlvCodecs._ +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, ShortChannelId, UInt64} +import scodec.bits.{BitVector, ByteVector, HexStringSyntax} /** - * Created by t-bast on 05/07/2019. - */ + * Created by t-bast on 05/07/2019. + */ + +case class OnionRoutingPacket(version: Int, publicKey: ByteVector, payload: ByteVector, hmac: ByteVector32) + +/** Tlv types used inside onion messages. */ +sealed trait OnionTlv extends Tlv + +object OnionTlv { + + /** Amount to forward to the next node. */ + case class AmountToForward(amount: MilliSatoshi) extends OnionTlv + + /** CLTV value to use for the HTLC offered to the next node. */ + case class OutgoingCltv(cltv: CltvExpiry) extends OnionTlv + + /** Id of the channel to use to forward a payment to the next node. */ + case class OutgoingChannelId(shortChannelId: ShortChannelId) extends OnionTlv + +} + +object Onion { + + import OnionTlv._ + + 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[OnionTlv] + } + + /** Per-hop payload from an HTLC's payment onion (after decryption and decoding). */ + sealed trait PerHopPayload + + /** Per-hop payload for an intermediate node. */ + sealed trait RelayPayload extends PerHopPayload with PerHopPayloadFormat { + /** Amount to forward to the next node. */ + val amountToForward: MilliSatoshi + /** CLTV value to use for the HTLC offered to the next node. */ + val outgoingCltv: CltvExpiry + /** Id of the channel to use to forward a payment to the next node. */ + val outgoingChannelId: ShortChannelId + } + + /** Per-hop payload for a final node. */ + sealed trait FinalPayload extends PerHopPayload with PerHopPayloadFormat { + val amount: MilliSatoshi + val expiry: CltvExpiry + } + + case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends RelayPayload with LegacyFormat -case class OnionRoutingPacket(version: Int, - publicKey: ByteVector, - payload: ByteVector, - hmac: ByteVector32) + case class FinalLegacyPayload(amount: MilliSatoshi, expiry: CltvExpiry) extends FinalPayload with LegacyFormat -case class PerHopPayload(shortChannelId: ShortChannelId, - amtToForward: Long, - outgoingCltvValue: Long) + case class RelayTlvPayload(records: TlvStream[OnionTlv]) extends RelayPayload with TlvFormat { + override val amountToForward = records.get[AmountToForward].get.amount + override val outgoingCltv = records.get[OutgoingCltv].get.cltv + override val outgoingChannelId = records.get[OutgoingChannelId].get.shortChannelId + } + + case class FinalTlvPayload(records: TlvStream[OnionTlv]) extends FinalPayload with TlvFormat { + override val amount = records.get[AmountToForward].get.amount + override val expiry = records.get[OutgoingCltv].get.cltv + } + +} object OnionCodecs { + import Onion._ + import OnionTlv._ + import scodec.codecs._ + import scodec.{Attempt, Codec, DecodeResult, Decoder, Err} + def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( ("version" | uint8) :: ("publicKey" | bytes(33)) :: ("onionPayload" | bytes(payloadLength)) :: - ("hmac" | CommonCodecs.bytes32)).as[OnionRoutingPacket] + ("hmac" | bytes32)).as[OnionRoutingPacket] val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(Sphinx.PaymentPacket.PayloadLength) - val perHopPayloadCodec: Codec[PerHopPayload] = ( - ("realm" | constant(ByteVector.fromByte(0))) :: - ("short_channel_id" | CommonCodecs.shortchannelid) :: - ("amt_to_forward" | CommonCodecs.uint64overflow) :: - ("outgoing_cltv_value" | uint32) :: - ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] - /** - * The 1.1 BOLT spec changed the onion frame format to use variable-length per-hop payloads. - * The first bytes contain a varint encoding the length of the payload data (not including the trailing mac). - * That varint is considered to be part of the payload, so the payload length includes the number of bytes used by - * the varint prefix. - */ + * The 1.1 BOLT spec changed the onion frame format to use variable-length per-hop payloads. + * The first bytes contain a varint encoding the length of the payload data (not including the trailing mac). + * That varint is considered to be part of the payload, so the payload length includes the number of bytes used by + * the varint prefix. + */ val payloadLengthDecoder = Decoder[Long]((bits: BitVector) => - CommonCodecs.varintoverflow.decode(bits).map(d => DecodeResult(d.value + (bits.length - d.remainder.length) / 8, d.remainder))) + varintoverflow.decode(bits).map(d => DecodeResult(d.value + (bits.length - d.remainder.length) / 8, d.remainder))) + + private val amountToForward: Codec[AmountToForward] = ("amount_msat" | tu64overflow).xmap(amountMsat => AmountToForward(MilliSatoshi(amountMsat)), (a: AmountToForward) => a.amount.toLong) + + private val outgoingCltv: Codec[OutgoingCltv] = ("cltv" | tu32).xmap(cltv => OutgoingCltv(CltvExpiry(cltv)), (c: OutgoingCltv) => c.cltv.toLong) + + private val outgoingChannelId: Codec[OutgoingChannelId] = (("length" | constant(hex"08")) :: ("short_channel_id" | shortchannelid)).as[OutgoingChannelId] + + private val onionTlvCodec = discriminated[OnionTlv].by(varint) + .typecase(UInt64(2), amountToForward) + .typecase(UInt64(4), outgoingCltv) + .typecase(UInt64(6), outgoingChannelId) + + val tlvPerHopPayloadCodec: Codec[TlvStream[OnionTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionTlv](onionTlvCodec).complete + + private val legacyRelayPerHopPayloadCodec: Codec[RelayLegacyPayload] = ( + ("realm" | constant(ByteVector.fromByte(0))) :: + ("short_channel_id" | shortchannelid) :: + ("amt_to_forward" | millisatoshi) :: + ("outgoing_cltv_value" | cltvExpiry) :: + ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[RelayLegacyPayload] + + private val legacyFinalPerHopPayloadCodec: Codec[FinalLegacyPayload] = ( + ("realm" | constant(ByteVector.fromByte(0))) :: + ("short_channel_id" | ignore(8 * 8)) :: + ("amount" | millisatoshi) :: + ("expiry" | cltvExpiry) :: + ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[FinalLegacyPayload] + + case class MissingRequiredTlv(tag: UInt64) extends Err { + // @formatter:off + val failureMessage: FailureMessage = InvalidOnionPayload(tag, 0) + override def message = failureMessage.message + override def context: List[String] = Nil + override def pushContext(ctx: String): Err = this + // @formatter:on + } + + val relayPerHopPayloadCodec: Codec[RelayPayload] = fallback(tlvPerHopPayloadCodec, legacyRelayPerHopPayloadCodec).narrow({ + case Left(tlvs) if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2))) + case Left(tlvs) if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) + case Left(tlvs) if tlvs.get[OutgoingChannelId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(6))) + case Left(tlvs) => Attempt.successful(RelayTlvPayload(tlvs)) + case Right(legacy) => Attempt.successful(legacy) + }, { + case legacy: RelayLegacyPayload => Right(legacy) + case RelayTlvPayload(tlvs) => Left(tlvs) + }) + + val finalPerHopPayloadCodec: Codec[FinalPayload] = fallback(tlvPerHopPayloadCodec, legacyFinalPerHopPayloadCodec).narrow({ + case Left(tlvs) if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2))) + case Left(tlvs) if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) + case Left(tlvs) => Attempt.successful(FinalTlvPayload(tlvs)) + case Right(legacy) => Attempt.successful(legacy) + }, { + case legacy: FinalLegacyPayload => Right(legacy) + case FinalTlvPayload(tlvs) => Left(tlvs) + }) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala new file mode 100644 index 0000000000..0dc5f57050 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryChannelRangeTlv.scala @@ -0,0 +1,37 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.CommonCodecs.{shortchannelid, varint, varintoverflow} +import scodec.Codec +import scodec.codecs._ + +sealed trait QueryChannelRangeTlv extends Tlv + +object QueryChannelRangeTlv { + /** + * Optional query flag that is appended to QueryChannelRange + * @param flag bit 1 set means I want timestamps, bit 2 set means I want checksums + */ + case class QueryFlags(flag: Long) extends QueryChannelRangeTlv { + val wantTimestamps = QueryFlags.wantTimestamps(flag) + + val wantChecksums = QueryFlags.wantChecksums(flag) + } + + case object QueryFlags { + val WANT_TIMESTAMPS: Long = 1 + val WANT_CHECKSUMS: Long = 2 + val WANT_ALL: Long = (WANT_TIMESTAMPS | WANT_CHECKSUMS) + + def wantTimestamps(flag: Long) = (flag & WANT_TIMESTAMPS) != 0 + + def wantChecksums(flag: Long) = (flag & WANT_CHECKSUMS) != 0 + } + + val queryFlagsCodec: Codec[QueryFlags] = Codec(("flag" | varintoverflow)).as[QueryFlags] + + val codec: Codec[TlvStream[QueryChannelRangeTlv]] = TlvCodecs.tlvStream(discriminated.by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, queryFlagsCodec)) + ) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala new file mode 100644 index 0000000000..12f5ad96fa --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/QueryShortChannelIdsTlv.scala @@ -0,0 +1,46 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.CommonCodecs.{varint, varintoverflow} +import scodec.Codec +import scodec.codecs.{byte, discriminated, list, provide, variableSizeBytesLong, zlib} + +sealed trait QueryShortChannelIdsTlv extends Tlv + +object QueryShortChannelIdsTlv { + + /** + * Optional TLV-based query message that can be appended to QueryShortChannelIds + * @param encoding 0 means uncompressed, 1 means compressed with zlib + * @param array array of query flags, each flags specifies the info we want for a given channel + */ + case class EncodedQueryFlags(encoding: EncodingType, array: List[Long]) extends QueryShortChannelIdsTlv + + case object QueryFlagType { + val INCLUDE_CHANNEL_ANNOUNCEMENT: Long = 1 + val INCLUDE_CHANNEL_UPDATE_1: Long = 2 + val INCLUDE_CHANNEL_UPDATE_2: Long = 4 + val INCLUDE_NODE_ANNOUNCEMENT_1: Long = 8 + val INCLUDE_NODE_ANNOUNCEMENT_2: Long = 16 + + def includeChannelAnnouncement(flag: Long) = (flag & INCLUDE_CHANNEL_ANNOUNCEMENT) != 0 + + def includeUpdate1(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_1) != 0 + + def includeUpdate2(flag: Long) = (flag & INCLUDE_CHANNEL_UPDATE_2) != 0 + + def includeNodeAnnouncement1(flag: Long) = (flag & INCLUDE_NODE_ANNOUNCEMENT_1) != 0 + + def includeNodeAnnouncement2(flag: Long) = (flag & INCLUDE_NODE_ANNOUNCEMENT_2) != 0 + } + + val encodedQueryFlagsCodec: Codec[EncodedQueryFlags] = + discriminated[EncodedQueryFlags].by(byte) + .\(0) { case a@EncodedQueryFlags(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(varintoverflow)).as[EncodedQueryFlags]) + .\(1) { case a@EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(varintoverflow))).as[EncodedQueryFlags]) + + + val codec: Codec[TlvStream[QueryShortChannelIdsTlv]] = TlvCodecs.tlvStream(discriminated.by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, encodedQueryFlagsCodec)) + ) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala new file mode 100644 index 0000000000..c7ea06fa67 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ReplyChannelRangeTlv.scala @@ -0,0 +1,64 @@ +package fr.acinq.eclair.wire + +import fr.acinq.eclair.{UInt64, wire} +import fr.acinq.eclair.wire.CommonCodecs.{varint, varintoverflow} +import scodec.Codec +import scodec.codecs._ + +sealed trait ReplyChannelRangeTlv extends Tlv + +object ReplyChannelRangeTlv { + + /** + * + * @param timestamp1 timestamp for node 1, or 0 + * @param timestamp2 timestamp for node 2, or 0 + */ + case class Timestamps(timestamp1: Long, timestamp2: Long) + + /** + * Optional timestamps TLV that can be appended to ReplyChannelRange + * + * @param encoding same convention as for short channel ids + * @param timestamps + */ + case class EncodedTimestamps(encoding: EncodingType, timestamps: List[Timestamps]) extends ReplyChannelRangeTlv + + /** + * + * @param checksum1 checksum for node 1, or 0 + * @param checksum2 checksum for node 2, or 0 + */ + case class Checksums(checksum1: Long, checksum2: Long) + + /** + * Optional checksums TLV that can be appended to ReplyChannelRange + * + * @param checksums + */ + case class EncodedChecksums(checksums: List[Checksums]) extends ReplyChannelRangeTlv + + val timestampsCodec: Codec[Timestamps] = ( + ("timestamp1" | uint32) :: + ("timestamp2" | uint32) + ).as[Timestamps] + + val encodedTimestampsCodec: Codec[EncodedTimestamps] = variableSizeBytesLong(varintoverflow, + discriminated[EncodedTimestamps].by(byte) + .\(0) { case a@EncodedTimestamps(EncodingType.UNCOMPRESSED, _) => a }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(timestampsCodec)).as[EncodedTimestamps]) + .\(1) { case a@EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, _) => a }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(timestampsCodec))).as[EncodedTimestamps]) + ) + + val checksumsCodec: Codec[Checksums] = ( + ("checksum1" | uint32) :: + ("checksum2" | uint32) + ).as[Checksums] + + val encodedChecksumsCodec: Codec[EncodedChecksums] = variableSizeBytesLong(varintoverflow, list(checksumsCodec)).as[EncodedChecksums] + + val innerCodec = discriminated[ReplyChannelRangeTlv].by(varint) + .typecase(UInt64(1), encodedTimestampsCodec) + .typecase(UInt64(3), encodedChecksumsCodec) + + val codec: Codec[TlvStream[ReplyChannelRangeTlv]] = TlvCodecs.tlvStream(innerCodec) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala index 664f7182ca..0f56ba666d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvCodecs.scala @@ -44,6 +44,14 @@ object TlvCodecs { .\(0x07) { case i if i < 0x0100000000000000L => i }(variableSizeUInt64(7, 0x01000000000000L)) .\(0x08) { case i if i <= UInt64.MaxValue => i }(variableSizeUInt64(8, 0x0100000000000000L)) + /** + * Length-prefixed truncated long (1 to 9 bytes unsigned integer). + * This codec can be safely used for values < `2^63` and will fail otherwise. + */ + val tu64overflow: Codec[Long] = tu64.exmap( + u => if (u <= Long.MaxValue) Attempt.Successful(u.toBigInt.toLong) else Attempt.Failure(Err(s"overflow for value $u")), + l => if (l >= 0) Attempt.Successful(UInt64(l)) else Attempt.Failure(Err(s"uint64 must be positive (actual=$l)"))) + /** * Length-prefixed truncated uint32 (1 to 5 bytes unsigned integer). */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala index e4eea124f8..3ea3d85c21 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/TlvTypes.scala @@ -19,14 +19,13 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.UInt64 import scodec.bits.ByteVector +import scala.reflect.ClassTag + /** * Created by t-bast on 20/06/2019. */ -// @formatter:off trait Tlv -sealed trait OnionTlv extends Tlv -// @formatter:on /** * Generic tlv type we fallback to if we don't understand the incoming tlv. @@ -45,9 +44,18 @@ case class GenericTlv(tag: UInt64, value: ByteVector) extends Tlv * @param unknown unknown tlv records. * @tparam T the stream namespace is a trait extending the top-level tlv trait. */ -case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[GenericTlv] = Nil) +case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[GenericTlv] = Nil) { + /** + * + * @tparam R input type parameter, must be a subtype of the main TLV type + * @return the TLV record of type that matches the input type parameter if any (there can be at most one, since BOLTs specify + * that TLV records are supposed to be unique) + */ + def get[R <: T : ClassTag]: Option[R] = records.collectFirst { case r: R => r } +} object TlvStream { + def empty[T <: Tlv] = TlvStream[T](Nil, Nil) def apply[T <: Tlv](records: T*): TlvStream[T] = TlvStream(records, Nil) diff --git a/eclair-core/src/main/scala/kamon/Kamon.scala b/eclair-core/src/main/scala/kamon/Kamon.scala new file mode 100644 index 0000000000..fe7670db69 --- /dev/null +++ b/eclair-core/src/main/scala/kamon/Kamon.scala @@ -0,0 +1,37 @@ +package kamon + +import kamon.context.Context + +/** + * Kamon does not work on Android and using it would not make sense anyway, we use this simplistic mocks instead + */ +object Kamon { + + object Mock { + def start() = this + + def stop() = this + + def finish() = this + + def withoutTags() = this + + def withTag(args: String*) = this + + def increment() = this + + def record(a: Long) = this + } + + def timer(name: String) = Mock + + def spanBuilder(name: String) = Mock + + def counter(name: String) = Mock + + def histogram(name: String) = Mock + + def runWithContextEntry[T, K](key: Context.Key[K], value: K)(f: => T): T = f + + def runWithSpan[T](span: Any, finishSpan: Boolean)(f: => T): T = f +} diff --git a/eclair-core/src/main/scala/kamon/context/Context.scala b/eclair-core/src/main/scala/kamon/context/Context.scala new file mode 100644 index 0000000000..ffee5b34b4 --- /dev/null +++ b/eclair-core/src/main/scala/kamon/context/Context.scala @@ -0,0 +1,7 @@ +package kamon.context + +object Context { + def key[T](name: String, emptyValue: T) = Key(name, emptyValue) + + case class Key[T](val name: String, val emptyValue: T) +} diff --git a/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java new file mode 100644 index 0000000000..e48c72cf9e --- /dev/null +++ b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java @@ -0,0 +1,40 @@ +/* + * 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; + +import fr.acinq.bitcoin.Satoshi; + +/** + * This class is a compile-time check that we are able to compile Java code that uses MilliSatoshi utilities. + */ +public final class MilliSatoshiTest { + + public static void Test() { + MilliSatoshi msat = new MilliSatoshi(561); + Satoshi sat = new Satoshi(1); + msat.truncateToSatoshi(); + msat = msat.max(sat); + msat = msat.min(sat); + MilliSatoshi.toMilliSatoshi(sat); + msat = MilliSatoshi.toMilliSatoshi(sat).$plus(msat); + msat = msat.$plus(msat); + msat = msat.$times(2.0); + Boolean check1 = msat.$less$eq(new MilliSatoshi(1105)); + Boolean check2 = msat.$greater(sat); + } + +} diff --git a/eclair-core/src/test/resources/api/usablebalances b/eclair-core/src/test/resources/api/usablebalances deleted file mode 100644 index edbd4e5a97..0000000000 --- a/eclair-core/src/test/resources/api/usablebalances +++ /dev/null @@ -1 +0,0 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSendMsat":100000000,"canReceiveMsat":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSendMsat":400000000,"canReceiveMsat":30000000,"isPublic":false}] \ No newline at end of file diff --git a/eclair-core/src/test/resources/application.conf b/eclair-core/src/test/resources/application.conf index 393fa212cb..b1d268b7c7 100644 --- a/eclair-core/src/test/resources/application.conf +++ b/eclair-core/src/test/resources/application.conf @@ -12,6 +12,6 @@ akka { test { # factor by which to scale timeouts during tests, e.g. to account for shared # build system load - timefactor = 3.0 + timefactor = 5.0 } } \ No newline at end of file diff --git a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected b/eclair-core/src/test/resources/scenarii/01-offer1.script.expected index c3444f106f..0fc65dcbb7 100644 --- a/eclair-core/src/test/resources/scenarii/01-offer1.script.expected +++ b/eclair-core/src/test/resources/scenarii/01-offer1.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) + Offered htlcs: (0,1000000 msat) Received htlcs: - Balance us: 999000000 - Balance them: 1000000000 + Balance us: 999000000 msat + Balance them: 1000000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) - Balance us: 1000000000 - Balance them: 999000000 + Received htlcs: (0,1000000 msat) + Balance us: 1000000000 msat + Balance them: 999000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) - Balance us: 1000000000 - Balance them: 999000000 + Received htlcs: (0,1000000 msat) + Balance us: 1000000000 msat + Balance them: 999000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) + Offered htlcs: (0,1000000 msat) Received htlcs: - Balance us: 999000000 - Balance them: 1000000000 + Balance us: 999000000 msat + Balance them: 1000000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected b/eclair-core/src/test/resources/scenarii/02-offer2.script.expected index b12ec47413..d31361786e 100644 --- a/eclair-core/src/test/resources/scenarii/02-offer2.script.expected +++ b/eclair-core/src/test/resources/scenarii/02-offer2.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) (1,2000000) + Offered htlcs: (0,1000000 msat) (1,2000000 msat) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: 997000000 msat + Balance them: 1000000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Received htlcs: (0,1000000 msat) (1,2000000 msat) + Balance us: 1000000000 msat + Balance them: 997000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 1: Offered htlcs: - Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Received htlcs: (0,1000000 msat) (1,2000000 msat) + Balance us: 1000000000 msat + Balance them: 997000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) (1,2000000) + Offered htlcs: (0,1000000 msat) (1,2000000 msat) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: 997000000 msat + Balance them: 1000000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected b/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected index 1ffcada1d7..a81561bc8f 100644 --- a/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected +++ b/eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected @@ -3,28 +3,28 @@ LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 999000000 - Balance them: 1001000000 + Balance us: 999000000 msat + Balance them: 1001000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 1001000000 - Balance them: 999000000 + Balance us: 1001000000 msat + Balance them: 999000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 1001000000 - Balance them: 999000000 + Balance us: 1001000000 msat + Balance them: 999000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: - Balance us: 999000000 - Balance them: 1001000000 + Balance us: 999000000 msat + Balance them: 1001000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected index f9eacd72d4..c309a14e41 100644 --- a/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected +++ b/eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected @@ -3,28 +3,28 @@ LOCAL COMMITS: Commit 1: Offered htlcs: (0,1000000) (1,2000000) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: 997000000 msat + Balance them: 1000000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: Offered htlcs: Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Balance us: 1000000000 msat + Balance them: 997000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: Offered htlcs: Received htlcs: (0,1000000) (1,2000000) - Balance us: 1000000000 - Balance them: 997000000 + Balance us: 1000000000 msat + Balance them: 997000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: Offered htlcs: (0,1000000) (1,2000000) Received htlcs: - Balance us: 997000000 - Balance them: 1000000000 + Balance us: 997000000 msat + Balance them: 1000000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected index dfbeddddb2..a259e358d1 100644 --- a/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected +++ b/eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 1: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,1000000 msat) + Received htlcs: (0,2000000 msat) + Balance us: 999000000 msat + Balance them: 998000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,2000000 msat) + Received htlcs: (0,1000000 msat) + Balance us: 998000000 msat + Balance them: 999000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,2000000 msat) + Received htlcs: (0,1000000 msat) + Balance us: 998000000 msat + Balance them: 999000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 1: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,1000000 msat) + Received htlcs: (0,2000000 msat) + Balance us: 999000000 msat + Balance them: 998000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected index bf1061fa9a..f47357ea01 100644 --- a/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected +++ b/eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected @@ -1,30 +1,30 @@ ***A*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,1000000 msat) + Received htlcs: (0,2000000 msat) + Balance us: 999000000 msat + Balance them: 998000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,2000000 msat) + Received htlcs: (0,1000000 msat) + Balance us: 998000000 msat + Balance them: 999000000 msat Fee rate: 10000 ***B*** LOCAL COMMITS: Commit 2: - Offered htlcs: (0,2000000) - Received htlcs: (0,1000000) - Balance us: 998000000 - Balance them: 999000000 + Offered htlcs: (0,2000000 msat) + Received htlcs: (0,1000000 msat) + Balance us: 998000000 msat + Balance them: 999000000 msat Fee rate: 10000 REMOTE COMMITS: Commit 2: - Offered htlcs: (0,1000000) - Received htlcs: (0,2000000) - Balance us: 999000000 - Balance them: 998000000 + Offered htlcs: (0,1000000 msat) + Received htlcs: (0,2000000 msat) + Balance us: 999000000 msat + Balance them: 998000000 msat Fee rate: 10000 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala new file mode 100644 index 0000000000..19f1d94602 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala @@ -0,0 +1,62 @@ +/* + * 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 + +import org.scalatest.{FunSuite, ParallelTestExecution} + +/** + * Created by t-bast on 21/08/2019. + */ + +class CltvExpirySpec extends FunSuite with ParallelTestExecution { + + test("cltv expiry delta") { + val d = CltvExpiryDelta(561) + assert(d.toInt === 561) + + // add + assert(d + 5 === CltvExpiryDelta(566)) + assert(d + CltvExpiryDelta(5) === CltvExpiryDelta(566)) + + // compare + assert(d <= CltvExpiryDelta(561)) + assert(d < CltvExpiryDelta(562)) + assert(d >= CltvExpiryDelta(561)) + assert(d > CltvExpiryDelta(560)) + + // convert to cltv expiry + assert(d.toCltvExpiry(blockHeight = 1105) === CltvExpiry(1666)) + assert(d.toCltvExpiry(blockHeight = 1106) === CltvExpiry(1667)) + } + + test("cltv expiry") { + val e = CltvExpiry(1105) + assert(e.toLong === 1105) + + // add + assert(e + CltvExpiryDelta(561) === CltvExpiry(1666)) + assert(e - CltvExpiryDelta(561) === CltvExpiry(544)) + assert(e - CltvExpiry(561) === CltvExpiryDelta(544)) + + // compare + assert(e <= CltvExpiry(1105)) + assert(e < CltvExpiry(1106)) + assert(e >= CltvExpiry(1105)) + assert(e > CltvExpiry(1104)) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala index ea294912a5..3ff9017b0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CoinUtilsSpec.scala @@ -16,11 +16,10 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Btc, MilliBtc, MilliSatoshi, Satoshi} -import org.scalatest.FunSuite +import fr.acinq.bitcoin.{Btc, MilliBtc, Satoshi} +import org.scalatest.{FunSuite, ParallelTestExecution} - -class CoinUtilsSpec extends FunSuite { +class CoinUtilsSpec extends FunSuite with ParallelTestExecution { test("Convert string amount to the correct BtcAmount") { val am_btc: MilliSatoshi = CoinUtils.convertStringAmountToMsat("1", BtcUnit.code) 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 67a7765dcb..29b297f475 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,38 +16,32 @@ package fr.acinq.eclair -import java.io.File - import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} -import fr.acinq.bitcoin.{ByteVector32, Crypto} import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.TestWallet +import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register, _} +import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.OpenChannel -import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment, SendPayment, SendPaymentToRoute} -import org.scalatest.{Outcome, fixture} -import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest +import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import org.scalatest.{Matchers, Outcome, fixture} -import scodec.bits._ -import TestConstants._ -import fr.acinq.eclair.channel.{CMD_FORCECLOSE, Register} -import fr.acinq.eclair.payment.LocalPaymentHandler -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db._ -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.{LocalPaymentHandler, PaymentRequest} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.{Outcome, ParallelTestExecution, fixture} +import scodec.bits._ import scala.concurrent.Await -import scala.util.{Failure, Success} import scala.concurrent.duration._ +import scala.util.Success -class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSuiteLike with IdiomaticMockito { +class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with IdiomaticMockito with ParallelTestExecution { - implicit val timeout = Timeout(30 seconds) + implicit val timeout: Timeout = Timeout(30 seconds) case class FixtureParam(register: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, kit: Kit) @@ -82,14 +76,14 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingSatoshis = 10000000L, pushMsat_opt = None, fundingFeerateSatByte_opt = Some(5), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeerateSatByte_opt = Some(5), flags_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt == Some(1250)) + assert(open.fundingTxFeeratePerKw_opt === Some(1250)) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingSatoshis = 10000000L, pushMsat_opt = None, fundingFeerateSatByte_opt = Some(1), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeerateSatByte_opt = Some(1), flags_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt == Some(MinimumFeeratePerKw)) + assert(open1.fundingTxFeeratePerKw_opt === Some(MinimumFeeratePerKw)) } test("call send with passing correct arguments") { f => @@ -98,38 +92,55 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val eclair = new EclairImpl(kit) val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None) - val send = paymentInitiator.expectMsgType[SendPayment] - assert(send.targetNodeId == nodeId) - assert(send.amountMsat == 123) - assert(send.paymentHash == ByteVector32.Zeroes) - assert(send.assistedRoutes == Seq.empty) + eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None) + val send = paymentInitiator.expectMsgType[SendPaymentRequest] + assert(send.externalId === None) + assert(send.targetNodeId === nodeId) + assert(send.amount === 123.msat) + assert(send.paymentHash === ByteVector32.Zeroes) + assert(send.paymentRequest === None) + assert(send.assistedRoutes === Seq.empty) // with assisted routes - val hints = Seq(Seq(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBaseMsat = 10, feeProportionalMillionths = 1, cltvExpiryDelta = 12))) - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = hints, minFinalCltvExpiry_opt = None) - val send1 = paymentInitiator.expectMsgType[SendPayment] - assert(send1.targetNodeId == nodeId) - assert(send1.amountMsat == 123) - assert(send1.paymentHash == ByteVector32.Zeroes) - assert(send1.assistedRoutes == hints) + 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, randomKey, "description", None, None, hints) + eclair.send(Some(externalId1), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice1)) + val send1 = paymentInitiator.expectMsgType[SendPaymentRequest] + assert(send1.externalId === Some(externalId1)) + assert(send1.targetNodeId === nodeId) + assert(send1.amount === 123.msat) + assert(send1.paymentHash === ByteVector32.Zeroes) + assert(send1.paymentRequest === Some(invoice1)) + assert(send1.assistedRoutes === hints) // with finalCltvExpiry - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = Some(96)) - val send2 = paymentInitiator.expectMsgType[SendPayment] - assert(send2.targetNodeId == nodeId) - assert(send2.amountMsat == 123) - assert(send2.paymentHash == ByteVector32.Zeroes) - assert(send2.finalCltvExpiry == 96) + val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" + val invoice2 = PaymentRequest("lntb", Some(123 msat), System.currentTimeMillis() / 1000L, nodeId, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty) + eclair.send(Some(externalId2), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice2)) + val send2 = paymentInitiator.expectMsgType[SendPaymentRequest] + assert(send2.externalId === Some(externalId2)) + assert(send2.targetNodeId === nodeId) + assert(send2.amount === 123.msat) + assert(send2.paymentHash === ByteVector32.Zeroes) + assert(send2.paymentRequest === Some(invoice2)) + assert(send2.finalExpiryDelta === CltvExpiryDelta(96)) // with custom route fees parameters - eclair.send(recipientNodeId = nodeId, amountMsat = 123, paymentHash = ByteVector32.Zeroes, assistedRoutes = Seq.empty, minFinalCltvExpiry_opt = None, feeThresholdSat_opt = Some(123), maxFeePct_opt = Some(4.20)) - val send3 = paymentInitiator.expectMsgType[SendPayment] - assert(send3.targetNodeId == nodeId) - assert(send3.amountMsat == 123) - assert(send3.paymentHash == ByteVector32.Zeroes) - assert(send3.routeParams.get.maxFeeBaseMsat == 123 * 1000) // conversion sat -> msat - assert(send3.routeParams.get.maxFeePct == 4.20) + eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None, feeThreshold_opt = Some(123 sat), maxFeePct_opt = Some(4.20)) + val send3 = paymentInitiator.expectMsgType[SendPaymentRequest] + assert(send3.externalId === None) + assert(send3.targetNodeId === nodeId) + assert(send3.amount === 123.msat) + assert(send3.paymentHash === ByteVector32.Zeroes) + assert(send3.routeParams.get.maxFeeBase === 123000.msat) // conversion sat -> msat + assert(send3.routeParams.get.maxFeePct === 4.20) + + val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;" + assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), nodeId, 123 msat, ByteVector32.Zeroes), 50 millis)) + + val expiredInvoice = invoice2.copy(timestamp = 0L) + assertThrows[IllegalArgumentException](Await.result(eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(expiredInvoice)), 50 millis)) } test("allupdates can filter by nodeId") { f => @@ -138,11 +149,11 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 13), - makeUpdate(4L, a, e, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdate(2L, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(3L, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(7L, e, c, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12) + makeUpdate(1L, a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(13)), + makeUpdate(4L, a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), + makeUpdate(2L, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(500)), + makeUpdate(3L, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(500)), + makeUpdate(7L, e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)) ).toMap val eclair = new EclairImpl(kit) @@ -186,15 +197,15 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val fallBackAddressRaw = "muhtvdmsnbQEPFuEmxcChX58fGvXaaUoVt" val eclair = new EclairImpl(kit) - eclair.receive("some desc", Some(123L), Some(456), Some(fallBackAddressRaw), None) + eclair.receive("some desc", Some(123 msat), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] - assert(receive.amountMsat_opt == Some(MilliSatoshi(123L))) - assert(receive.expirySeconds_opt == Some(456)) - assert(receive.fallbackAddress == Some(fallBackAddressRaw)) + assert(receive.amount_opt === Some(123 msat)) + assert(receive.expirySeconds_opt === Some(456)) + assert(receive.fallbackAddress === Some(fallBackAddressRaw)) // try with wrong address format - assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123L), Some(456), Some("wassa wassa"), None)) + assertThrows[IllegalArgumentException](eclair.receive("some desc", Some(123 msat), Some(456), Some("wassa wassa"), None)) } test("passing a payment_preimage to /createinvoice should result in an invoice with payment_hash=H(payment_preimage)") { fixture => @@ -204,7 +215,7 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val eclair = new EclairImpl(kitWithPaymentHandler) val paymentPreimage = randomBytes32 - val fResp = eclair.receive(description = "some desc", amountMsat_opt = None, expire_opt = None, fallbackAddress_opt = None, paymentPreimage_opt = Some(paymentPreimage)) + val fResp = eclair.receive(description = "some desc", amount_opt = None, expire_opt = None, fallbackAddress_opt = None, paymentPreimage_opt = Some(paymentPreimage)) awaitCond({ fResp.value match { case Some(Success(pr)) => pr.paymentHash == Crypto.sha256(paymentPreimage) @@ -224,7 +235,7 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu auditDb.listSent(anyLong, anyLong) returns Seq.empty auditDb.listReceived(anyLong, anyLong) returns Seq.empty auditDb.listRelayed(anyLong, anyLong) returns Seq.empty - paymentDb.listPaymentRequests(anyLong, anyLong) returns Seq.empty + paymentDb.listIncomingPayments(anyLong, anyLong) returns Seq.empty val databases = mock[Databases] databases.audit returns auditDb @@ -234,15 +245,15 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val eclair = new EclairImpl(kitWithMockAudit) Await.result(eclair.networkFees(None, None), 10 seconds) - auditDb.listNetworkFees(0, MaxEpochSeconds).wasCalled(once) // assert the call was made only once and with the specified params + auditDb.listNetworkFees(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) // assert the call was made only once and with the specified params Await.result(eclair.audit(None, None), 10 seconds) - auditDb.listRelayed(0, MaxEpochSeconds).wasCalled(once) - auditDb.listReceived(0, MaxEpochSeconds).wasCalled(once) - auditDb.listSent(0, MaxEpochSeconds).wasCalled(once) + auditDb.listRelayed(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) + auditDb.listReceived(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) + auditDb.listSent(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) Await.result(eclair.allInvoices(None, None), 10 seconds) - paymentDb.listPaymentRequests(0, MaxEpochSeconds).wasCalled(once) // assert the call was made only once and with the specified params + paymentDb.listIncomingPayments(0, TimestampQueryFilters.MaxEpochMilliseconds).wasCalled(once) // assert the call was made only once and with the specified params } test("sendtoroute should pass the parameters correctly") { f => @@ -250,15 +261,14 @@ class EclairImplSpec extends TestKit(ActorSystem("mySystem")) with fixture.FunSu val route = Seq(PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87")) val eclair = new EclairImpl(kit) - eclair.sendToRoute(route, 1234, ByteVector32.One, 123) - - val send = paymentInitiator.expectMsgType[SendPaymentToRoute] - - assert(send.hops == route) - assert(send.amountMsat == 1234) - assert(send.finalCltvExpiry == 123) - assert(send.paymentHash == ByteVector32.One) + eclair.sendToRoute(Some("42"), route, 1234 msat, ByteVector32.One, CltvExpiryDelta(123)) + + val send = paymentInitiator.expectMsgType[SendPaymentRequest] + assert(send.externalId === Some("42")) + assert(send.predefinedRoute === route) + assert(send.amount === 1234.msat) + assert(send.finalExpiryDelta === CltvExpiryDelta(123)) + assert(send.paymentHash === ByteVector32.One) } - } 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 102e50e532..30f06c385d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -16,9 +16,6 @@ package fr.acinq.eclair -import java.nio.ByteOrder - -import fr.acinq.bitcoin.Protocol import fr.acinq.eclair.Features._ import org.scalatest.FunSuite import scodec.bits._ @@ -38,21 +35,25 @@ class FeaturesSpec extends FunSuite { assert(hasFeature(hex"02", Features.OPTION_DATA_LOSS_PROTECT_OPTIONAL)) } - test("'initial_routing_sync' and 'data_loss_protect' feature") { - val features = hex"0a" - assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) + test("'initial_routing_sync', 'data_loss_protect' and 'variable_length_onion' features") { + val features = hex"010a" + assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL) && hasFeature(features, VARIABLE_LENGTH_ONION_MANDATORY)) } test("'variable_length_onion' feature") { assert(hasFeature(hex"0100", Features.VARIABLE_LENGTH_ONION_MANDATORY)) + assert(hasVariableLengthOnion(hex"0100")) assert(hasFeature(hex"0200", Features.VARIABLE_LENGTH_ONION_OPTIONAL)) + assert(hasVariableLengthOnion(hex"0200")) } test("features compatibility") { - assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1l << VARIABLE_LENGTH_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) + assert(areSupported(ByteVector.fromLong(1L << INITIAL_ROUTING_SYNC_BIT_OPTIONAL))) + assert(areSupported(ByteVector.fromLong(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY))) + assert(areSupported(ByteVector.fromLong(1L << OPTION_DATA_LOSS_PROTECT_OPTIONAL))) + assert(areSupported(ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_OPTIONAL))) + assert(areSupported(ByteVector.fromLong(1L << VARIABLE_LENGTH_ONION_MANDATORY))) + assert(areSupported(hex"0b")) assert(!areSupported(hex"14")) assert(!areSupported(hex"0141")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/JsonSerializersSpec.scala index 0099b668f8..7464b58a91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/JsonSerializersSpec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint} +import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi} import fr.acinq.eclair.channel.{ChannelVersion, LocalChanges, LocalParams, RemoteParams} import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions._ @@ -37,7 +37,7 @@ class JsonSerializersSpec extends FunSuite with Logging { } test("ChannelVersion serialization") { - assert(write(ChannelVersion.STANDARD) === """"00000000000000000000000000000000"""") + assert(write(ChannelVersion.STANDARD) === """"00000000000000000000000000000001"""") } test("Direction serialization") { @@ -48,12 +48,12 @@ class JsonSerializersSpec extends FunSuite with Logging { test("serialize LocalParams") { val localParams = LocalParams( nodeId = randomKey.publicKey, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L, 42L, 42L, 42L)), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), - toSelfDelay = Random.nextInt(Short.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), isFunder = Random.nextBoolean(), @@ -67,11 +67,11 @@ class JsonSerializersSpec extends FunSuite with Logging { test("serialize RemoteParams") { val remoteParams = RemoteParams( nodeId = randomKey.publicKey, - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), - toSelfDelay = Random.nextInt(Short.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), fundingPubKey = randomKey.publicKey, revocationBasepoint = randomKey.publicKey, @@ -85,13 +85,13 @@ class JsonSerializersSpec extends FunSuite with Logging { } test("serialize CommitmentSpec") { - val spec = CommitmentSpec(Set(DirectedHtlc(IN, UpdateAddHtlc(randomBytes32, 421, 1245, randomBytes32, 1000, OnionRoutingPacket(0, randomKey.publicKey.value, hex"0101", randomBytes32)))), feeratePerKw = 1233, toLocalMsat = 100, toRemoteMsat = 200) + val spec = CommitmentSpec(Set(DirectedHtlc(IN, UpdateAddHtlc(randomBytes32, 421, MilliSatoshi(1245), randomBytes32, CltvExpiry(1000), OnionRoutingPacket(0, randomKey.publicKey.value, hex"0101", randomBytes32)))), feeratePerKw = 1233, toLocal = MilliSatoshi(100), toRemote = MilliSatoshi(200)) logger.info(write(spec)) } test("serialize LocalChanges") { val channelId = randomBytes32 - val add = UpdateAddHtlc(channelId, 421, 1245, randomBytes32, 1000, OnionRoutingPacket(0, randomKey.publicKey.value, hex"0101", randomBytes32)) + val add = UpdateAddHtlc(channelId, 421, MilliSatoshi(1245), randomBytes32, CltvExpiry(1000), OnionRoutingPacket(0, randomKey.publicKey.value, hex"0101", randomBytes32)) val fail = UpdateFailHtlc(channelId, 42, hex"0101") val failMalformed = UpdateFailMalformedHtlc(channelId, 42, randomBytes32, 1) val updateFee = UpdateFee(channelId, 1500) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala new file mode 100644 index 0000000000..ae0cb5236c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala @@ -0,0 +1,78 @@ +/* + * 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 + +import fr.acinq.bitcoin.Satoshi +import org.scalatest.FunSuite + +/** + * Created by t-bast on 22/08/2019. + */ + +class MilliSatoshiSpec extends FunSuite { + + 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)) + + // 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)) + + // multiply + 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)) + + // compare + assert(MilliSatoshi(561) <= MilliSatoshi(561)) + assert(MilliSatoshi(561) <= 1105.msat) + assert(MilliSatoshi(561) < MilliSatoshi(1105)) + assert(MilliSatoshi(561) >= MilliSatoshi(561)) + assert(MilliSatoshi(1105) >= MilliSatoshi(561)) + assert(MilliSatoshi(1105) > MilliSatoshi(561)) + assert(MilliSatoshi(1000) <= Satoshi(1)) + assert(MilliSatoshi(1000) <= 2.sat) + assert(MilliSatoshi(1000) < Satoshi(2)) + assert(MilliSatoshi(1000) >= Satoshi(1)) + assert(MilliSatoshi(2000) >= Satoshi(1)) + 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)) + + // 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)) + } + +} 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 3dd302c3c9..8e13db4ffb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -17,15 +17,15 @@ package fr.acinq.eclair import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, Script} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, Satoshi, Script} import org.scalatest.FunSuite import scodec.bits._ import scala.util.Try /** - * Created by PM on 27/01/2017. - */ + * Created by PM on 27/01/2017. + */ class PackageSpec extends FunSuite { @@ -34,7 +34,7 @@ class PackageSpec extends FunSuite { (hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 1, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE") :: (hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000", 2, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0002") :: (hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00F0", 0x0F00, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FF0") :: Nil) - .map(x => (ByteVector32(x._1), x._2, ByteVector32(x._3))) + .map(x => (ByteVector32(x._1), x._2, ByteVector32(x._3))) data.foreach(x => assert(toLongId(ByteVector32(x._1), x._2) === x._3)) } @@ -113,4 +113,5 @@ class PackageSpec extends FunSuite { assert(ShortChannelId(Long.MaxValue - 1) < ShortChannelId(Long.MaxValue)) assert(ShortChannelId(Long.MaxValue) < ShortChannelId(Long.MaxValue + 1)) } + } 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 da1edf35f7..e0931dba52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -16,10 +16,13 @@ package fr.acinq.eclair +import java.util.concurrent.atomic.AtomicLong + import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Block import fr.acinq.eclair.crypto.LocalKeyManager import org.scalatest.FunSuite + import scala.util.Try class StartupSpec extends FunSuite { @@ -42,8 +45,10 @@ class StartupSpec extends FunSuite { val conf = illegalAliasConf.withFallback(ConfigFactory.parseResources("reference.conf").getConfig("eclair")) val keyManager = new LocalKeyManager(seed = randomBytes32, chainHash = Block.TestnetGenesisBlock.hash) + val blockCount = new AtomicLong(0) + // try to create a NodeParams instance with a conf that contains an illegal alias - val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb(), new TestConstants.TestFeeEstimator)) + val nodeParamsAttempt = Try(NodeParams.makeNodeParams(conf, keyManager, None, TestConstants.inMemoryDb(), blockCount, new TestConstants.TestFeeEstimator)) assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) } 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 3f5e0b3744..f68d3575e8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair import java.sql.{Connection, DriverManager} +import java.util.concurrent.atomic.AtomicLong import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Script} @@ -26,18 +27,19 @@ import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf -import fr.acinq.eclair.wire.{Color, NodeAddress} -import scodec.bits.ByteVector +import fr.acinq.eclair.wire.{Color, EncodingType, NodeAddress} +import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.duration._ /** - * Created by PM on 26/04/2016. - */ + * Created by PM on 26/04/2016. + */ object TestConstants { - val fundingSatoshis = 1000000L - val pushMsat = 200000000L + val globalFeatures = hex"0200" // variable_length_onion + val fundingSatoshis = 1000000L sat + val pushMsat = 200000000L msat val feeratePerKw = 10000L val emptyOnionPacket = wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) @@ -64,28 +66,31 @@ object TestConstants { // This is a function, and not a val! When called will return a new NodeParams def nodeParams = NodeParams( keyManager = keyManager, + blockCount = new AtomicLong(400000), alias = "alice", color = Color(1, 2, 3), publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, - globalFeatures = ByteVector.empty, - localFeatures = ByteVector(0), + globalFeatures = globalFeatures, + localFeatures = ByteVector.fromValidHex("088a"), overrideFeatures = Map.empty, - dustLimitSatoshis = 1100, + syncWhitelist = Set.empty, + dustLimit = 1100 sat, onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), feeEstimator = new TestFeeEstimator, maxFeerateMismatch = 1.5, + closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1 ), maxHtlcValueInFlightMsat = UInt64(150000000), maxAcceptedHtlcs = 100, - expiryDeltaBlocks = 144, - fulfillSafetyBeforeTimeoutBlocks = 6, - htlcMinimumMsat = 0, + expiryDeltaBlocks = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeoutBlocks = CltvExpiryDelta(6), + htlcMinimum = 0 msat, minDepthBlocks = 3, - toRemoteDelayBlocks = 144, - maxToLocalDelayBlocks = 1000, - feeBaseMsat = 546000, + toRemoteDelayBlocks = CltvExpiryDelta(144), + maxToLocalDelayBlocks = CltvExpiryDelta(1000), + feeBase = 546000 msat, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, @@ -101,14 +106,19 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - minFundingSatoshis = 1000L, + minFundingSatoshis = 1000 sat, routerConf = RouterConf( randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - searchMaxFeeBaseSat = 21, + networkStatsRefreshInterval = 1 hour, + requestNodeAnnouncements = true, + encodingType = EncodingType.COMPRESSED_ZLIB, + channelRangeChunkSize = 20, + channelQueryChunkSize = 5, + searchMaxFeeBase = 21 sat, searchMaxFeePct = 0.03, - searchMaxCltv = 2016, + searchMaxCltv = CltvExpiryDelta(2016), searchMaxRouteLength = 20, searchHeuristicsEnabled = false, searchRatioCltv = 0.0, @@ -124,7 +134,7 @@ object TestConstants { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), isFunder = true, fundingSatoshis).copy( - channelReserveSatoshis = 10000 // Bob will need to keep that much satoshis as direct payment + channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment ) } @@ -134,28 +144,31 @@ object TestConstants { def nodeParams = NodeParams( keyManager = keyManager, + blockCount = new AtomicLong(400000), alias = "bob", color = Color(4, 5, 6), publicAddresses = NodeAddress.fromParts("localhost", 9732).get :: Nil, - globalFeatures = ByteVector.empty, + globalFeatures = globalFeatures, localFeatures = ByteVector.empty, // no announcement overrideFeatures = Map.empty, - dustLimitSatoshis = 1000, + syncWhitelist = Set.empty, + dustLimit = 1000 sat, onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), feeEstimator = new TestFeeEstimator, maxFeerateMismatch = 1.0, + closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1 ), maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs maxAcceptedHtlcs = 30, - expiryDeltaBlocks = 144, - fulfillSafetyBeforeTimeoutBlocks = 6, - htlcMinimumMsat = 1000, + expiryDeltaBlocks = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeoutBlocks = CltvExpiryDelta(6), + htlcMinimum = 1000 msat, minDepthBlocks = 3, - toRemoteDelayBlocks = 144, - maxToLocalDelayBlocks = 1000, - feeBaseMsat = 546000, + toRemoteDelayBlocks = CltvExpiryDelta(144), + maxToLocalDelayBlocks = CltvExpiryDelta(1000), + feeBase = 546000 msat, feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, @@ -171,14 +184,19 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - minFundingSatoshis = 1000L, + minFundingSatoshis = 1000 sat, routerConf = RouterConf( randomizeRouteSelection = false, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, - searchMaxFeeBaseSat = 21, + networkStatsRefreshInterval = 1 hour, + requestNodeAnnouncements = true, + encodingType = EncodingType.UNCOMPRESSED, + channelRangeChunkSize = 20, + channelQueryChunkSize = 5, + searchMaxFeeBase = 21 sat, searchMaxFeePct = 0.03, - searchMaxCltv = 2016, + searchMaxCltv = CltvExpiryDelta(2016), searchMaxRouteLength = 20, searchHeuristicsEnabled = false, searchRatioCltv = 0.0, @@ -194,7 +212,7 @@ object TestConstants { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), isFunder = false, fundingSatoshis).copy( - channelReserveSatoshis = 20000 // Alice will need to keep that much satoshis as direct payment + channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment ) } 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 4f5ecdcb6f..8ee0ccf73e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair import java.io.File +import java.net.ServerSocket object TestUtils { @@ -27,4 +28,17 @@ object TestUtils { .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 + + def availablePort: Int = synchronized { + var serverSocket: ServerSocket = null + try { + serverSocket = new ServerSocket(0) + serverSocket.getLocalPort + } finally { + if (serverSocket != null) { + serverSocket.close() + } + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala index 0aaa6fd811..ca88758426 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala @@ -30,10 +30,6 @@ import scala.concurrent.Await */ abstract class TestkitBaseClass extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterEach with BeforeAndAfterAll { - override def beforeAll { - Globals.blockCount.set(400000) - } - override def afterEach() { system.actorSelection(system / "*") ! PoisonPill intercept[ActorNotFound] { 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 def42f49ba..dfa1e606a6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala @@ -22,23 +22,63 @@ import scodec.bits._ class UInt64Spec extends FunSuite { - test("handle values from 0 to 2^63-1") { + test("handle values from 0 to 2^64-1") { val a = UInt64(hex"0xffffffffffffffff") val b = UInt64(hex"0xfffffffffffffffe") val c = UInt64(42) val z = UInt64(0) + val l = UInt64(Long.MaxValue) + val l1 = UInt64(hex"8000000000000000") // Long.MaxValue + 1 + assert(a > b) + assert(a.toBigInt > b.toBigInt) assert(b < a) - assert(z < a && z < b && z < c) + assert(b.toBigInt < a.toBigInt) + assert(l.toBigInt < l1.toBigInt) + assert(z < a && z < b && z < c && z < l && c < l && l < l1 && l < b && l < a) assert(a == a) - assert(a.toByteVector === hex"0xffffffffffffffff") - assert(a.toString === "18446744073709551615") - assert(b.toByteVector === hex"0xfffffffffffffffe") + assert(a == UInt64.MaxValue) + + assert(l.toByteVector == hex"7fffffffffffffff") + assert(l.toString == Long.MaxValue.toString) + assert(l.toBigInt == BigInt(Long.MaxValue)) + + assert(l1.toByteVector == hex"8000000000000000") + 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(b.toByteVector === hex"fffffffffffffffe") assert(b.toString === "18446744073709551614") - assert(c.toByteVector === hex"0x2a") + assert(b.toBigInt === BigInt("18446744073709551614")) + + assert(c.toByteVector === hex"00000000000002a") assert(c.toString === "42") - assert(z.toByteVector === hex"0x00") + assert(c.toBigInt === BigInt("42")) + + 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") } + test("use unsigned comparison when comparing millisatoshis to uint64") { + assert(UInt64(123) <= MilliSatoshi(123) && UInt64(123) >= MilliSatoshi(123)) + assert(UInt64(123) < MilliSatoshi(1234)) + assert(UInt64(1234) > MilliSatoshi(123)) + assert(UInt64(hex"ffffffffffffffff") > MilliSatoshi(123)) + assert(UInt64(hex"ffffffffffffffff") > MilliSatoshi(-123)) + assert(UInt64(hex"7ffffffffffffffe") < MilliSatoshi(Long.MaxValue)) // 7ffffffffffffffe == Long.MaxValue - 1 + assert(UInt64(hex"7fffffffffffffff") <= MilliSatoshi(Long.MaxValue) && UInt64(hex"7fffffffffffffff") >= MilliSatoshi(Long.MaxValue)) // 7fffffffffffffff == Long.MaxValue + assert(UInt64(hex"8000000000000000") > MilliSatoshi(Long.MaxValue)) // 8000000000000000 == Long.MaxValue + 1 + assert(UInt64(1) > MilliSatoshi(-1)) + assert(UInt64(0) > MilliSatoshi(Long.MinValue)) + } + + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala index 825a609efa..9e0baf76f9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala @@ -16,15 +16,15 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.{ByteVector32, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction, TxIn, TxOut} +import fr.acinq.eclair.LongToBtcAmount import scodec.bits.ByteVector import scala.concurrent.Future -import scala.util.Try /** - * Created by PM on 06/07/2017. - */ + * Created by PM on 06/07/2017. + */ class TestWallet extends EclairWallet { var rolledback = Set.empty[Transaction] @@ -53,6 +53,6 @@ object TestWallet { txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) - MakeFundingTxResponse(fundingTx, 0, Satoshi(420)) + MakeFundingTxResponse(fundingTx, 0, 420 sat) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 7f48d5241a..9b067c74df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,15 +21,15 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{ByteVector32, Block, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{Block, ByteVector32, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.{addressToPublicKeyScript, randomKey} +import fr.acinq.eclair.{LongToBtcAmount, TestConstants, addressToPublicKeyScript, randomKey} import grizzled.slf4j.Logging -import org.json4s.JsonAST._ -import org.json4s.{DefaultFormats, JString} +import org.json4s.JsonAST.{JString, _} +import org.json4s.DefaultFormats import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.collection.JavaConversions._ @@ -45,15 +45,15 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe "eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", - "eclair.bitcoind.port" -> 28333, - "eclair.bitcoind.rpcport" -> 28332, + "eclair.bitcoind.port" -> bitcoindPort, + "eclair.bitcoind.rpcport" -> bitcoindRpcPort, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString - implicit val formats = DefaultFormats.withBigDecimal + implicit val formats = DefaultFormats override def beforeAll(): Unit = { startBitcoind() @@ -125,7 +125,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe TxIn(OutPoint(unknownTxids(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), TxIn(OutPoint(unknownTxids(2), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) ), - txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, + txOut = TxOut(1000000 sat, addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0) // signing it should fail, and the error message should contain the txids of the UTXOs that could not be used @@ -145,8 +145,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val sender = TestProbe() wallet.getBalance.pipeTo(sender.ref) - //val foo = sender.receiveOne(2 seconds) - assert(sender.expectMsgType[Satoshi] > Satoshi(0)) + assert(sender.expectMsgType[Satoshi] > 0.sat) wallet.getFinalAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] @@ -206,7 +205,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val sender = TestProbe() wallet.getBalance.pipeTo(sender.ref) - assert(sender.expectMsgType[Satoshi] > Satoshi(0)) + assert(sender.expectMsgType[Satoshi] > 0.sat) wallet.getFinalAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] @@ -233,7 +232,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe assert(sender.expectMsgType[Boolean]) wallet.getBalance.pipeTo(sender.ref) - assert(sender.expectMsgType[Satoshi] > Satoshi(0)) + assert(sender.expectMsgType[Satoshi] > 0.sat) } test("detect if tx has been doublespent") { @@ -258,7 +257,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val tx1 = Transaction.read(signedTx1) // 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 - bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount.toLong).sum * 1.0 / 1e8)).pipeTo(sender.ref) + bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount).sum.toLong * 1.0 / 1e8)).pipeTo(sender.ref) val JString(unsignedtx2) = sender.expectMsgType[JValue] bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" @@ -276,8 +275,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe wallet.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(false) // let's confirm tx2 - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1) // this time tx1 has been double spent wallet.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(true) 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 518e6ed1a7..b432d00df7 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 @@ -28,10 +28,11 @@ import fr.acinq.eclair.TestUtils import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import grizzled.slf4j.Logging -import org.json4s.JsonAST.JValue +import org.json4s.JsonAST.{JArray, JDecimal, JInt, JString, JValue} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +import scala.io.Source trait BitcoindService extends Logging { self: TestKitBase => @@ -39,12 +40,20 @@ trait BitcoindService extends Logging { implicit val system: ActorSystem implicit val sttpBackend = OkHttpFutureBackend() + val bitcoindPort: Int = TestUtils.availablePort + + val bitcoindRpcPort: Int = TestUtils.availablePort + + val bitcoindZmqBlockPort: Int = TestUtils.availablePort + + val bitcoindZmqTxPort: Int = TestUtils.availablePort + import scala.sys.process._ val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.17.1/bin/bitcoind") + val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.18.1/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null @@ -56,11 +65,17 @@ trait BitcoindService extends Logging { def startBitcoind(): Unit = { Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) { - Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, StandardCopyOption.REPLACE_EXISTING) + val is = classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf") + val conf = Source.fromInputStream(is).mkString + .replace("28333", bitcoindPort.toString) + .replace("28332", bitcoindRpcPort.toString) + .replace("28334", bitcoindZmqBlockPort.toString) + .replace("28335", bitcoindZmqTxPort.toString) + Files.writeString(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, conf) } bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() - bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332) + bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = bitcoindRpcPort) bitcoincli = system.actorOf(Props(new Actor { override def receive: Receive = { case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender @@ -83,11 +98,35 @@ trait BitcoindService extends Logging { logger.info(s"waiting for bitcoind to initialize...") awaitCond({ sender.send(bitcoincli, BitcoinReq("getnetworkinfo")) - sender.receiveOne(5 second).isInstanceOf[JValue] - }, max = 30 seconds, interval = 500 millis) + sender.expectMsgType[Any](5 second) match { + case j: JValue => j \ "version" match { + case JInt(_) => true + case _ => false + } + case _ => false + } + }, max = 3 minutes, interval = 2 seconds) logger.info(s"generating initial blocks...") - sender.send(bitcoincli, BitcoinReq("generate", 150)) - sender.expectMsgType[JValue](30 seconds) + generateBlocks(bitcoincli, 150) + awaitCond({ + sender.send(bitcoincli, BitcoinReq("getbalance")) + val JDecimal(balance) = sender.expectMsgType[JDecimal](30 seconds) + balance > 100 + }, max = 3 minutes, interval = 2 second) + } + + def generateBlocks(bitcoinCli: ActorRef, blockCount: Int, address: Option[String] = None, timeout: FiniteDuration = 10 seconds)(implicit system: ActorSystem): Unit = { + val sender = TestProbe() + val addressToUse = address match { + case Some(addr) => addr + case None => + sender.send(bitcoinCli, BitcoinReq("getnewaddress")) + val JString(address) = sender.expectMsgType[JValue](timeout) + address + } + sender.send(bitcoinCli, BitcoinReq("generatetoaddress", blockCount, addressToUse)) + val JArray(blocks) = sender.expectMsgType[JValue](timeout) + assert(blocks.size == blockCount) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index 945c6db054..7d01d052b8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -24,8 +24,8 @@ import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Transaction import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} import grizzled.slf4j.Logging -import org.json4s.JsonAST._ -import org.json4s.{DefaultFormats, JString} +import org.json4s.JsonAST.{JString, _} +import org.json4s.{DefaultFormats} import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scala.collection.JavaConversions._ @@ -38,8 +38,8 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi "eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", - "eclair.bitcoind.port" -> 28333, - "eclair.bitcoind.rpcport" -> 28332, + "eclair.bitcoind.port" -> bitcoindPort, + "eclair.bitcoind.rpcport" -> bitcoindRpcPort, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") @@ -88,7 +88,9 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi client.publishTransaction(tx).pipeTo(sender.ref) sender.expectMsg(txid) // let's confirm the tx - bitcoinClient.invoke("generate", 1).pipeTo(sender.ref) + sender.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(generatingAddress) = sender.expectMsgType[JValue] + bitcoinClient.invoke("generatetoaddress", 1, generatingAddress).pipeTo(sender.ref) sender.expectMsgType[JValue] // and publish the tx a third time to test idempotence client.publishTransaction(tx).pipeTo(sender.ref) @@ -110,7 +112,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi client.publishTransaction(tx).pipeTo(sender.ref) sender.expectMsg(txid) // let's confirm the tx - bitcoinClient.invoke("generate", 1).pipeTo(sender.ref) + bitcoinClient.invoke("generatetoaddress", 1, generatingAddress).pipeTo(sender.ref) sender.expectMsgType[JValue] // and publish the tx a fifth time to test idempotence client.publishTransaction(tx).pipeTo(sender.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala index 7227c5f59d..0aca5ca3ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.blockchain.electrum import java.net.InetSocketAddress +import java.util.concurrent.atomic.AtomicLong import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} @@ -66,7 +67,7 @@ class ElectrumClientPoolSpec extends TestKit(ActorSystem("test")) with FunSuiteL val addresses = random.shuffle(serverAddresses.toSeq).take(2).toSet + ElectrumClientPool.ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT) stream.close() assert(addresses.nonEmpty) - pool = system.actorOf(Props(new ElectrumClientPool(addresses)), "electrum-client") + pool = system.actorOf(Props(new ElectrumClientPool(new AtomicLong(), addresses)), "electrum-client") } test("connect to an electrumx mainnet server") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala index 375840f74c..40884b8e86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala @@ -22,23 +22,22 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey} import fr.acinq.bitcoin._ import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb -import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.{Scripts, Transactions} import grizzled.slf4j.Logging import org.scalatest.FunSuite import scodec.bits.ByteVector import scala.util.{Failure, Random, Success, Try} - class ElectrumWalletBasicSpec extends FunSuite with Logging { import ElectrumWallet._ import ElectrumWalletBasicSpec._ val swipeRange = 10 - val dustLimit = 546 satoshi + val dustLimit = 546 sat val feeRatePerKw = 20000 - val minimumFee = Satoshi(2000) + val minimumFee = 2000 sat val master = DeterministicWallet.generate(ByteVector32(ByteVector.fill(32)(1))) val accountMaster = accountKey(master, Block.RegtestGenesisBlock.hash) @@ -133,10 +132,10 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { } test("use actual transaction weight to compute fees") { - val state1 = addFunds(state, (state.accountKeys(0), Satoshi(5000000)) :: (state.accountKeys(1), Satoshi(6000000)) :: (state.accountKeys(2), Satoshi(4000000)) :: Nil) + val state1 = addFunds(state, (state.accountKeys(0), 5000000 sat) :: (state.accountKeys(1), 6000000 sat) :: (state.accountKeys(2), 4000000 sat) :: Nil) { - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(5000000), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) + val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000000 sat, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) val (state3, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true) val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) assert(fee == fee1) @@ -144,7 +143,7 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { assert(isFeerateOk(actualFeeRate, feeRatePerKw)) } { - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(5000000) - dustLimit, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) + val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000000.sat - dustLimit, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) val (state3, tx1, fee1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true) val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) assert(fee == fee1) @@ -153,7 +152,7 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { } { // with a huge fee rate that will force us to use an additional input when we complete our tx - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(3000000), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) + val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(3000000 sat, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) val (state3, tx1, fee1) = state1.completeTransaction(tx, 100 * feeRatePerKw, minimumFee, dustLimit, true) val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) assert(fee == fee1) @@ -176,13 +175,30 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { val state2 = addFunds(state1, state1.accountKeys(1), 2 btc) val state3 = addFunds(state2, state2.changeKeys(0), 0.5 btc) assert(state3.utxos.length == 3) - assert(state3.balance == (Satoshi(350000000),Satoshi(0))) + assert(state3.balance == (350000000 sat, 0 sat)) val (tx, fee) = state3.spendAll(Script.pay2wpkh(ByteVector.fill(20)(1)), feeRatePerKw) val Some((received, sent, Some(fee1))) = state3.computeTransactionDelta(tx) - assert(received == Satoshi(0)) + assert(received === 0.sat) + assert(fee == fee1) + assert(tx.txOut.map(_.amount).sum + fee == state3.balance._1 + state3.balance._2) + } + + test("check that issue #1146 is fixed") { + val state3 = addFunds(state, state.changeKeys(0), 0.5 btc) + + val pub1 = state.accountKeys(0).publicKey + val pub2 = state.accountKeys(1).publicKey + val redeemScript = Scripts.multiSig2of2(pub1, pub2) + val pubkeyScript = Script.pay2wsh(redeemScript) + val (tx, fee) = state3.spendAll(pubkeyScript, feeRatePerKw = 750) + val Some((received, sent, Some(fee1))) = state3.computeTransactionDelta(tx) + assert(received === 0.sat) assert(fee == fee1) assert(tx.txOut.map(_.amount).sum + fee == state3.balance._1 + state3.balance._2) + + val tx1 = Transaction(version = 2, txIn = Nil, txOut = TxOut(tx.txOut.map(_.amount).sum, pubkeyScript) :: Nil, lockTime = 0) + assert(Try(state3.completeTransaction(tx1, 750, 0 sat, dustLimit, true)).isSuccess) } test("fuzzy test") { @@ -190,15 +206,16 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { (0 to 10) foreach { _ => val funds = for (i <- 0 until random.nextInt(10)) yield { val index = random.nextInt(state.accountKeys.length) - val amount = dustLimit + Satoshi(random.nextInt(10000000)) + val amount = dustLimit + random.nextInt(10000000).sat (state.accountKeys(index), amount) } val state1 = addFunds(state, funds) (0 until 30) foreach { _ => - val amount = dustLimit + Satoshi(random.nextInt(10000000)) + val amount = dustLimit + random.nextInt(10000000).sat val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) Try(state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, true)) match { - case Success((state2, tx1, fee1)) => () + case Success((state2, tx1, fee1)) => + tx1.txOut.foreach(o => require(o.amount >= dustLimit, "output is below dust limit")) case Failure(cause) if cause.getMessage != null && cause.getMessage.contains("insufficient funds") => () case Failure(cause) => logger.error(s"unexpected $cause") } @@ -209,10 +226,10 @@ class ElectrumWalletBasicSpec extends FunSuite with Logging { object ElectrumWalletBasicSpec { /** - * - * @param actualFeeRate actual fee rate - * @param targetFeeRate target fee rate - * @return true if actual fee rate is within 10% of target - */ + * + * @param actualFeeRate actual fee rate + * @param targetFeeRate target fee rate + * @return true if actual fee rate is within 10% of target + */ def isFeerateOk(actualFeeRate: Long, targetFeeRate: Long): Boolean = Math.abs(actualFeeRate - targetFeeRate) < 0.1 * (actualFeeRate + targetFeeRate) } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala index 99d5e9ba48..a8e960c91b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala @@ -25,6 +25,7 @@ import akka.testkit.{TestActor, TestFSMRef, TestKit, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair.LongToBtcAmount import fr.acinq.eclair.blockchain.bitcoind.rpc.Error import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ @@ -35,10 +36,10 @@ import scodec.bits.ByteVector import scala.annotation.tailrec import scala.concurrent.duration._ - class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { import ElectrumWalletSimulatedClientSpec._ + val sender = TestProbe() val entropy = ByteVector32(ByteVector.fill(32)(1)) @@ -70,7 +71,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit } }) - val walletParameters = WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000)) + val walletParameters = WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = 5000 sat) val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, walletParameters)) // wallet sends a receive address notification as soon as it is created @@ -176,7 +177,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(1)).toHex) client.expectMsg(GetScriptHashHistory(scriptHash)) - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(100000), ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) + val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(100000 sat, ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil) // wallet will generate a new address and the corresponding subscription @@ -209,9 +210,9 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED) val ready = reconnect - assert(ready.unconfirmedBalance == Satoshi(0)) + assert(ready.unconfirmedBalance === 0.sat) } - + test("clear status when we have pending history requests") { while (client.msgAvailable) { client.receiveOne(100 milliseconds) @@ -239,7 +240,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(2)).toHex) client.expectMsg(GetScriptHashHistory(scriptHash)) - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(100000), ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) + val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(100000 sat, ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil) // wallet will generate a new address and the corresponding subscription @@ -262,8 +263,8 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit val firstChangeKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector val data1 = Data(walletParameters, Blockchain.fromGenesisBlock(Block.RegtestGenesisBlock.hash, Block.RegtestGenesisBlock.header), firstAccountKeys, firstChangeKeys) - val amount1 = Satoshi(1000000) - val amount2 = Satoshi(1500000) + val amount1 = 1000000 sat + val amount2 = 1500000 sat // transactions that send funds to our wallet val wallettxs = Seq( @@ -278,7 +279,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit val tx1 = { val tx = Transaction(version = 2, txIn = TxIn(OutPoint(wallettxs(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = walletOutput(wallettxs(0).txOut(0).amount - Satoshi(50000), data2.accountKeys(2).publicKey) :: walletOutput(Satoshi(50000), data2.changeKeys(0).publicKey) :: Nil, + txOut = walletOutput(wallettxs(0).txOut(0).amount - 50000.sat, data2.accountKeys(2).publicKey) :: walletOutput(50000 sat, data2.changeKeys(0).publicKey) :: Nil, lockTime = 0) data2.signTransaction(tx) } @@ -287,7 +288,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit val tx2 = { val tx = Transaction(version = 2, txIn = TxIn(OutPoint(wallettxs(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(wallettxs(1).txOut(0).amount - Satoshi(50000), Script.pay2wpkh(fr.acinq.eclair.randomKey.publicKey)) :: walletOutput(Satoshi(50000), data2.changeKeys(1).publicKey) :: Nil, + txOut = TxOut(wallettxs(1).txOut(0).amount - 50000.sat, Script.pay2wpkh(fr.acinq.eclair.randomKey.publicKey)) :: walletOutput(50000 sat, data2.changeKeys(1).publicKey) :: Nil, lockTime = 0) data2.signTransaction(tx) } @@ -303,7 +304,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit client.setAutoPilot(new testkit.TestActor.AutoPilot { override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = { counter = msg match { - case _:ScriptHashSubscription => counter + case _: ScriptHashSubscription => counter case _ => counter + 1 } msg match { @@ -376,9 +377,9 @@ object ElectrumWalletSimulatedClientSpec { def walletOutput(amount: Satoshi, key: PublicKey) = TxOut(amount, ElectrumWallet.computePublicKeyScript(key)) - def addOutputs(tx: Transaction, amount: Satoshi, keys: PublicKey*): Transaction = keys.foldLeft(tx) { case (t, k) => t.copy(txOut = t.txOut :+ walletOutput(amount, k)) } + def addOutputs(tx: Transaction, amount: Satoshi, keys: PublicKey*): Transaction = keys.foldLeft(tx) { case (t, k) => t.copy(txOut = t.txOut :+ walletOutput(amount, k)) } - def addToHistory(history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], scriptHash: ByteVector32, item : TransactionHistoryItem): Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]] = { + def addToHistory(history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], scriptHash: ByteVector32, item: TransactionHistoryItem): Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]] = { history.get(scriptHash) match { case None => history + (scriptHash -> List(item)) case Some(items) if items.contains(item) => history @@ -386,7 +387,7 @@ object ElectrumWalletSimulatedClientSpec { } } - def updateStatus(data: ElectrumWallet.Data) : ElectrumWallet.Data = { + def updateStatus(data: ElectrumWallet.Data): ElectrumWallet.Data = { val status1 = data.history.mapValues(items => { val status = items.map(i => s"${i.tx_hash}:${i.height}:").mkString("") Crypto.sha256(ByteVector.view(status.getBytes())).toString() @@ -394,7 +395,7 @@ object ElectrumWalletSimulatedClientSpec { data.copy(status = status1) } - def addTransaction(data: ElectrumWallet.Data, tx: Transaction) : ElectrumWallet.Data = { + def addTransaction(data: ElectrumWallet.Data, tx: Transaction): ElectrumWallet.Data = { data.transactions.get(tx.txid) match { case Some(_) => data case None => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala index 4771d98fe4..8a042474d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala @@ -16,19 +16,22 @@ package fr.acinq.eclair.blockchain.electrum - import java.net.InetSocketAddress import java.sql.DriverManager +import java.util.concurrent.atomic.AtomicLong import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import com.whisk.docker.DockerReadyChecker -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.eclair.LongToBtcAmount import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL} import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb +import fr.acinq.eclair.transactions.{Scripts, Transactions} +import fr.acinq.{bitcoin, eclair} import grizzled.slf4j.Logging import org.json4s.JsonAST.{JDecimal, JString, JValue} import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} @@ -38,7 +41,6 @@ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ - class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { import ElectrumWallet._ @@ -82,14 +84,13 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.receiveOne(5 second).isInstanceOf[JValue] }, max = 30 seconds, interval = 500 millis) logger.info(s"generating initial blocks...") - sender.send(bitcoincli, BitcoinReq("generate", 150)) - sender.expectMsgType[JValue](30 seconds) + generateBlocks(bitcoincli, 150, timeout = 30 seconds) DockerReadyChecker.LogLineContains("INFO:BlockProcessor:height: 151").looped(attempts = 15, delay = 1 second) } test("wait until wallet is ready") { - electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(ElectrumServerAddress(new InetSocketAddress("localhost", electrumPort), SSL.OFF))))) - wallet = system.actorOf(Props(new ElectrumWallet(seed, electrumClient, WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000)))), "wallet") + electrumClient = system.actorOf(Props(new ElectrumClientPool(new AtomicLong(), Set(ElectrumServerAddress(new InetSocketAddress("localhost", electrumPort), SSL.OFF))))) + wallet = system.actorOf(Props(new ElectrumWallet(seed, electrumClient, WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = 5000 sat))), "wallet") val probe = TestProbe() awaitCond({ probe.send(wallet, GetData) @@ -113,16 +114,14 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike awaitCond({ val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) - unconfirmed1 == unconfirmed + Satoshi(100000000L) + unconfirmed1 == unconfirmed + 100000000.sat }, max = 30 seconds, interval = 1 second) // confirm our tx - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] - + generateBlocks(bitcoincli, 1) awaitCond({ val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) - confirmed1 == confirmed + Satoshi(100000000L) + confirmed1 == confirmed + 100000000.sat }, max = 30 seconds, interval = 1 second) val GetCurrentReceiveAddressResponse(address1) = getCurrentAddress(probe) @@ -133,13 +132,11 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike logger.info(s"sending 0.5 btc to $address1") probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1, 0.5)) probe.expectMsgType[JValue] - - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] + generateBlocks(bitcoincli, 1) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) - confirmed1 == confirmed + Satoshi(250000000L) + val GetBalanceResponse(confirmed1, _) = getBalance(probe) + confirmed1 == confirmed + 250000000.sat }, max = 30 seconds, interval = 1 second) } @@ -149,7 +146,7 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike logger.info(s"initial balance: $confirmed $unconfirmed") // send money to our wallet - val amount = Satoshi(750000) + val amount = 750000 sat val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) val tx = Transaction(version = 2, txIn = Nil, @@ -159,22 +156,20 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike ), lockTime = 0L) val btcWallet = new BitcoinCoreWallet(bitcoinrpcclient) val future = for { - FundTransactionResponse(tx1, pos, fee) <- btcWallet.fundTransaction(tx, false, 10000) + FundTransactionResponse(tx1, _, _) <- btcWallet.fundTransaction(tx, false, 10000) SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1) txid <- btcWallet.publishTransaction(tx2) } yield txid - val txid = Await.result(future, 10 seconds) + Await.result(future, 10 seconds) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) + val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) unconfirmed1 == unconfirmed + amount + amount }, max = 30 seconds, interval = 1 second) - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] - + generateBlocks(bitcoincli, 1) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) + val GetBalanceResponse(confirmed1, _) = getBalance(probe) confirmed1 == confirmed + amount + amount }, max = 30 seconds, interval = 1 second) } @@ -193,27 +188,25 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val JString(txid) = probe.expectMsgType[JValue] logger.info(s"$txid sent 1 btc to us at $address") awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) - unconfirmed1 - unconfirmed == Satoshi(100000000L) + val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) + unconfirmed1 - unconfirmed === 100000000L.sat }, max = 30 seconds, interval = 1 second) val TransactionReceived(tx, 0, received, sent, _, _) = listener.receiveOne(5 seconds) assert(tx.txid === ByteVector32.fromValidHex(txid)) - assert(received === Satoshi(100000000)) + assert(received === 100000000.sat) logger.info("generating a new block") - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] - + generateBlocks(bitcoincli, 1) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) - confirmed1 - confirmed == Satoshi(100000000L) + val GetBalanceResponse(confirmed1, _) = getBalance(probe) + confirmed1 - confirmed === 100000000.sat }, max = 30 seconds, interval = 1 second) awaitCond({ val msg = listener.receiveOne(5 seconds) msg match { - case TransactionConfidenceChanged(txid, 1, _) => true + case TransactionConfidenceChanged(_, 1, _) => true case _ => false } }, max = 30 seconds, interval = 1 second) @@ -221,22 +214,21 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike test("send money to someone else (we broadcast)") { val probe = TestProbe() - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) + val GetBalanceResponse(confirmed, _) = getBalance(probe) // create a tx that sends money to Bitcoin Core's address probe.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(address) = probe.expectMsgType[JValue] val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L) probe.send(wallet, CompleteTransaction(tx, 20000)) - val CompleteTransactionResponse(tx1, fee1, None) = probe.expectMsgType[CompleteTransactionResponse] + val CompleteTransactionResponse(tx1, _, None) = probe.expectMsgType[CompleteTransactionResponse] // send it ourselves logger.info(s"sending 1 btc to $address with tx ${tx1.txid}") probe.send(wallet, BroadcastTransaction(tx1)) val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] + generateBlocks(bitcoincli, 1) awaitCond({ probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address)) @@ -245,9 +237,9 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike }, max = 30 seconds, interval = 1 second) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) + val GetBalanceResponse(confirmed1, _) = getBalance(probe) logger.debug(s"current balance is $confirmed1") - confirmed1 < confirmed - Btc(1) && confirmed1 > confirmed - Btc(1) - Satoshi(50000) + confirmed1 < confirmed - 1.btc && confirmed1 > confirmed - 1.btc - 50000.sat }, max = 30 seconds, interval = 1 second) } @@ -268,13 +260,12 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(wallet, BroadcastTransaction(tx1)) val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] - probe.send(bitcoincli, BitcoinReq("generate", 1)) - probe.expectMsgType[JValue] + generateBlocks(bitcoincli, 1) awaitCond({ - val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe) + val GetBalanceResponse(confirmed1, _) = getBalance(probe) logger.info(s"current balance is $confirmed $unconfirmed") - confirmed1 < confirmed - Btc(1) && confirmed1 > confirmed - Btc(1) - Satoshi(50000) + confirmed1 < confirmed - 1.btc && confirmed1 > confirmed - 1.btc - 50000.sat }, max = 30 seconds, interval = 1 second) } @@ -326,8 +317,7 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(wallet, IsDoubleSpent(tx2)) probe.expectMsg(IsDoubleSpentResponse(tx2, false)) - probe.send(bitcoincli, BitcoinReq("generate", 2)) - probe.expectMsgType[JValue] + generateBlocks(bitcoincli, 2) awaitCond({ probe.send(wallet, GetData) @@ -341,4 +331,63 @@ class ElectrumWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLike probe.send(wallet, IsDoubleSpent(tx2)) probe.expectMsg(IsDoubleSpentResponse(tx2, true)) } + + test("use all available balance") { + val probe = TestProbe() + + // send all our funds to ourself, so we have only one utxo which is the worse case here + val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) + probe.send(wallet, SendAll(Script.write(eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)), 750)) + val SendAllResponse(tx, _) = probe.expectMsgType[SendAllResponse] + probe.send(wallet, BroadcastTransaction(tx)) + val BroadcastTransactionResponse(`tx`, None) = probe.expectMsgType[BroadcastTransactionResponse] + + generateBlocks(bitcoincli, 1) + + awaitCond({ + probe.send(wallet, GetData) + val data = probe.expectMsgType[GetDataResponse].state + data.utxos.length == 1 && data.utxos(0).outPoint.txid == tx.txid + }, max = 30 seconds, interval = 1 second) + + + // send everything to a multisig 2-of-2, with the smallest possible fee rate + val priv = eclair.randomKey + val script = Script.pay2wsh(Scripts.multiSig2of2(priv.publicKey, priv.publicKey)) + probe.send(wallet, SendAll(Script.write(script), eclair.MinimumFeeratePerKw)) + val SendAllResponse(tx1, _) = probe.expectMsgType[SendAllResponse] + probe.send(wallet, BroadcastTransaction(tx1)) + val BroadcastTransactionResponse(`tx1`, None) = probe.expectMsgType[BroadcastTransactionResponse] + + generateBlocks(bitcoincli, 1) + + awaitCond({ + probe.send(wallet, GetData) + val data = probe.expectMsgType[GetDataResponse].state + data.utxos.isEmpty + }, max = 30 seconds, interval = 1 second) + + // send everything back to ourselves again + val tx2 = Transaction(version = 2, + txIn = TxIn(OutPoint(tx1, 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txOut = TxOut(Satoshi(0), eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, + lockTime = 0) + + val sig = Transaction.signInput(tx2, 0, Scripts.multiSig2of2(priv.publicKey, priv.publicKey), bitcoin.SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv) + val tx3 = tx2.updateWitness(0, ScriptWitness(Seq(ByteVector.empty, sig, sig, Script.write(Scripts.multiSig2of2(priv.publicKey, priv.publicKey))))) + Transaction.correctlySpends(tx3, Seq(tx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val fee = Transactions.weight2fee(tx3.weight(), 253) + val tx4 = tx3.copy(txOut = tx3.txOut(0).copy(amount = tx1.txOut(0).amount - fee) :: Nil) + val sig1 = Transaction.signInput(tx4, 0, Scripts.multiSig2of2(priv.publicKey, priv.publicKey), bitcoin.SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv) + val tx5 = tx4.updateWitness(0, ScriptWitness(Seq(ByteVector.empty, sig1, sig1, Script.write(Scripts.multiSig2of2(priv.publicKey, priv.publicKey))))) + + probe.send(wallet, BroadcastTransaction(tx5)) + val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] + + awaitCond({ + probe.send(wallet, GetData) + val data = probe.expectMsgType[GetDataResponse].state + data.utxos.length == 1 && data.utxos(0).outPoint.txid == tx5.txid + }, max = 30 seconds, interval = 1 second) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala index a5f963bd77..507ab3161f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala @@ -17,25 +17,27 @@ package fr.acinq.eclair.blockchain.electrum import java.net.InetSocketAddress +import java.util.concurrent.atomic.AtomicLong import akka.actor.{ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, ByteVector32, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{Base58, ByteVector32, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.eclair.LongToBtcAmount import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress -import fr.acinq.eclair.blockchain.{GetTxWithMetaResponse, GetTxWithMeta, WatchConfirmed, WatchEventConfirmed, WatchEventSpent, WatchSpent} +import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT} import grizzled.slf4j.Logging +import org.json4s import org.json4s.JsonAST.{JArray, JString, JValue} import org.scalatest.{BeforeAndAfterAll, FunSuiteLike} import scodec.bits._ import scala.concurrent.duration._ - -class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { +class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { override def beforeAll(): Unit = { logger.info("starting bitcoind") @@ -55,8 +57,9 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike test("watch for confirmed transactions") { val probe = TestProbe() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(electrumAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient))) + val blockCount = new AtomicLong() + val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) + val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) probe.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(address) = probe.expectMsgType[JValue] @@ -70,9 +73,7 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val listener = TestProbe() probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut(0).publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) - probe.send(bitcoincli, BitcoinReq("generate", 3)) - listener.expectNoMsg(1 second) - probe.send(bitcoincli, BitcoinReq("generate", 2)) + generateBlocks(bitcoincli, 5) val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds) assert(confirmed.tx.txid.toHex === txid) system.stop(watcher) @@ -80,8 +81,9 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike test("watch for spent transactions") { val probe = TestProbe() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(electrumAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient))) + val blockCount = new AtomicLong() + val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) + val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) probe.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(address) = probe.expectMsgType[JValue] @@ -103,7 +105,7 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val spendingTx = { val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx, pos), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(tx.txOut(pos).amount - Satoshi(1000), publicKeyScript = Script.pay2wpkh(priv.publicKey)) :: Nil, + txOut = TxOut(tx.txOut(pos).amount - 1000.sat, publicKeyScript = Script.pay2wpkh(priv.publicKey)) :: Nil, lockTime = 0) val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(priv.publicKey), SIGHASH_ALL, tx.txOut(pos).amount, SigVersion.SIGVERSION_WITNESS_V0, priv) val signedTx = tmp.updateWitness(0, ScriptWitness(sig :: priv.publicKey.value :: Nil)) @@ -116,17 +118,16 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike listener.expectNoMsg(1 second) probe.send(bitcoincli, BitcoinReq("sendrawtransaction", spendingTx.toString)) probe.expectMsgType[JValue] - probe.send(bitcoincli, BitcoinReq("generate", 2)) - val blocks = probe.expectMsgType[JValue] - val JArray(List(JString(block1), JString(block2))) = blocks - val spent = listener.expectMsgType[WatchEventSpent](20 seconds) + generateBlocks(bitcoincli, 2) + listener.expectMsgType[WatchEventSpent](20 seconds) system.stop(watcher) } test("get transaction") { + val blockCount = new AtomicLong() val mainnetAddress = ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT) - val electrumClient = system.actorOf(Props(new ElectrumClientPool(Set(mainnetAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient))) + val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(mainnetAddress)))) + val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) //Thread.sleep(10000) val probe = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala index ba1266f04a..71b487f3e8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala @@ -20,26 +20,28 @@ import com.spotify.docker.client.{DefaultDockerClient, DockerClient} import com.whisk.docker.impl.spotify.SpotifyDockerFactory import com.whisk.docker.scalatest.DockerTestKit import com.whisk.docker.{DockerContainer, DockerFactory} +import fr.acinq.eclair.TestUtils +import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import org.scalatest.Suite trait ElectrumxService extends DockerTestKit { - self: Suite => + self: Suite with BitcoindService => - val electrumPort = 47000 + val electrumPort = TestUtils.availablePort val electrumxContainer = if (System.getProperty("os.name").startsWith("Linux")) { // "host" mode will let the container access the host network on linux // we use our own docker image because other images on Docker lag behind and don't yet support 1.4 DockerContainer("acinq/electrumx") .withNetworkMode("host") - .withEnv("DAEMON_URL=http://foo:bar@localhost:28332", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") + .withEnv(s"DAEMON_URL=http://foo:bar@localhost:$bitcoindRpcPort", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") //.withLogLineReceiver(LogLineReceiver(true, println)) } else { // on windows or oxs, host mode is not available, but from docker 18.03 on host.docker.internal can be used instead // host.docker.internal is not (yet ?) available on linux though DockerContainer("acinq/electrumx") .withPorts(electrumPort -> Some(electrumPort)) - .withEnv("DAEMON_URL=http://foo:bar@host.docker.internal:28332", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") + .withEnv(s"DAEMON_URL=http://foo:bar@host.docker.internal:$bitcoindRpcPort", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") //.withLogLineReceiver(LogLineReceiver(true, println)) } 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 b129f52192..1ad9f555ff 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 @@ -42,8 +42,8 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco "eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", - "eclair.bitcoind.port" -> 28333, - "eclair.bitcoind.rpcport" -> 28332, + "eclair.bitcoind.port" -> bitcoindPort, + "eclair.bitcoind.rpcport" -> bitcoindRpcPort, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") @@ -120,7 +120,7 @@ class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with Bitco override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = method match { case "estimatesmartfee" => val blocks = params(0).asInstanceOf[Int] - val feerate = satoshi2btc(Satoshi(fees(blocks))).amount + val feerate = satoshi2btc(Satoshi(fees(blocks))).toBigDecimal Future(JObject(List("feerate" -> JDecimal(feerate), "blocks" -> JInt(blocks)))) case _ => Future.failed(new RuntimeException(s"Test BasicBitcoinJsonRPCClient: method $method is not supported")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala index 6f789a34a7..9718a499a1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala @@ -71,7 +71,7 @@ class BitgoFeeProviderSpec extends FunSuite { test("make sure API hasn't changed") { import scala.concurrent.duration._ - implicit val system = ActorSystem() + implicit val system = ActorSystem("test") implicit val ec = system.dispatcher implicit val sttp = OkHttpFutureBackend() implicit val timeout = Timeout(30 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala new file mode 100644 index 0000000000..56131fbb31 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -0,0 +1,10 @@ +package fr.acinq.eclair.channel + +import org.scalatest.FunSuite + +class ChannelTypesSpec extends FunSuite { + test("standard channel features include deterministic channel key path") { + assert(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + assert(!ChannelVersion.ZEROES.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + } +} 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 new file mode 100644 index 0000000000..9b8be1b0d5 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -0,0 +1,382 @@ +/* + * 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 + +import java.util.UUID + +import fr.acinq.eclair.channel.Commitments._ +import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.payment.Local +import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails +import fr.acinq.eclair.{TestkitBaseClass, _} +import org.scalatest.Outcome + +import scala.concurrent.duration._ + +class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods { + + type FixtureParam = SetupFixture + + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + within(30 seconds) { + reachNormal(setup, test.tags) + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(setup)) + } + } + + test("take additional HTLC fee into account") { f => + import f._ + val htlcOutputFee = 1720000 msat + val a = 772760000 msat // initial balance alice + val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + // we need to take the additional HTLC fee into account because balances are above the trim threshold. + assert(ac0.availableBalanceForSend == a - htlcOutputFee) + assert(bc0.availableBalanceForReceive == a - htlcOutputFee) + + val (_, cmdAdd) = makeCmdAdd(a - htlcOutputFee - 1000.msat, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) + val bc1 = receiveAdd(bc0, add) + val (_, commit1) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager) + val (bc2, _) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager) + // we don't take into account the additional HTLC fee since Alice's balance is below the trim threshold. + assert(ac1.availableBalanceForSend == 1000.msat) + assert(bc2.availableBalanceForReceive == 1000.msat) + } + + test("correct values for availableForSend/availableForReceive (success case)") { f => + import f._ + + val fee = 1720000 msat // fee due to the additional htlc output + val a = (772760000 msat) - fee // initial balance alice + val b = 190000000 msat // initial balance bob + val p = 42000000 msat // a->b payment + + val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + + assert(ac0.availableBalanceForSend > p) // alice can afford the payment + assert(ac0.availableBalanceForSend == a) + assert(ac0.availableBalanceForReceive == b) + assert(bc0.availableBalanceForSend == b) + assert(bc0.availableBalanceForReceive == a) + + val (payment_preimage, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) + assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) + assert(ac1.availableBalanceForReceive == b) + + val bc1 = receiveAdd(bc0, add) + assert(bc1.availableBalanceForSend == b) + assert(bc1.availableBalanceForReceive == a - p - fee) + + val (ac2, commit1) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager) + assert(ac2.availableBalanceForSend == a - p - fee) + assert(ac2.availableBalanceForReceive == b) + + val (bc2, revocation1) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager) + assert(bc2.availableBalanceForSend == b) + assert(bc2.availableBalanceForReceive == a - p - fee) + + val (ac3, _) = receiveRevocation(ac2, revocation1) + assert(ac3.availableBalanceForSend == a - p - fee) + assert(ac3.availableBalanceForReceive == b) + + val (bc3, commit2) = sendCommit(bc2, bob.underlyingActor.nodeParams.keyManager) + assert(bc3.availableBalanceForSend == b) + assert(bc3.availableBalanceForReceive == a - p - fee) + + val (ac4, revocation2) = receiveCommit(ac3, commit2, alice.underlyingActor.nodeParams.keyManager) + assert(ac4.availableBalanceForSend == a - p - fee) + assert(ac4.availableBalanceForReceive == b) + + val (bc4, _) = receiveRevocation(bc3, revocation2) + assert(bc4.availableBalanceForSend == b) + assert(bc4.availableBalanceForReceive == a - p - fee) + + val cmdFulfill = CMD_FULFILL_HTLC(0, payment_preimage) + val (bc5, fulfill) = sendFulfill(bc4, cmdFulfill) + assert(bc5.availableBalanceForSend == b + p) // as soon as we have the fulfill, the balance increases + assert(bc5.availableBalanceForReceive == a - p - fee) + + val Right((ac5, _, _)) = receiveFulfill(ac4, fulfill) + assert(ac5.availableBalanceForSend == a - p - fee) + assert(ac5.availableBalanceForReceive == b + p) + + val (bc6, commit3) = sendCommit(bc5, bob.underlyingActor.nodeParams.keyManager) + assert(bc6.availableBalanceForSend == b + p) + assert(bc6.availableBalanceForReceive == a - p - fee) + + val (ac6, revocation3) = receiveCommit(ac5, commit3, alice.underlyingActor.nodeParams.keyManager) + assert(ac6.availableBalanceForSend == a - p) + assert(ac6.availableBalanceForReceive == b + p) + + val (bc7, _) = receiveRevocation(bc6, revocation3) + assert(bc7.availableBalanceForSend == b + p) + assert(bc7.availableBalanceForReceive == a - p) + + val (ac7, commit4) = sendCommit(ac6, alice.underlyingActor.nodeParams.keyManager) + assert(ac7.availableBalanceForSend == a - p) + assert(ac7.availableBalanceForReceive == b + p) + + val (bc8, revocation4) = receiveCommit(bc7, commit4, bob.underlyingActor.nodeParams.keyManager) + assert(bc8.availableBalanceForSend == b + p) + assert(bc8.availableBalanceForReceive == a - p) + + val (ac8, _) = receiveRevocation(ac7, revocation4) + assert(ac8.availableBalanceForSend == a - p) + assert(ac8.availableBalanceForReceive == b + p) + } + + test("correct values for availableForSend/availableForReceive (failure case)") { f => + import f._ + + val fee = 1720000 msat // fee due to the additional htlc output + val a = (772760000 msat) - fee // initial balance alice + val b = 190000000 msat // initial balance bob + val p = 42000000 msat // a->b payment + + val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + + assert(ac0.availableBalanceForSend > p) // alice can afford the payment + assert(ac0.availableBalanceForSend == a) + assert(ac0.availableBalanceForReceive == b) + assert(bc0.availableBalanceForSend == b) + assert(bc0.availableBalanceForReceive == a) + + val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) + assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) + assert(ac1.availableBalanceForReceive == b) + + val bc1 = receiveAdd(bc0, add) + assert(bc1.availableBalanceForSend == b) + assert(bc1.availableBalanceForReceive == a - p - fee) + + val (ac2, commit1) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager) + assert(ac2.availableBalanceForSend == a - p - fee) + assert(ac2.availableBalanceForReceive == b) + + val (bc2, revocation1) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager) + assert(bc2.availableBalanceForSend == b) + assert(bc2.availableBalanceForReceive == a - p - fee) + + val (ac3, _) = receiveRevocation(ac2, revocation1) + assert(ac3.availableBalanceForSend == a - p - fee) + assert(ac3.availableBalanceForReceive == b) + + val (bc3, commit2) = sendCommit(bc2, bob.underlyingActor.nodeParams.keyManager) + assert(bc3.availableBalanceForSend == b) + assert(bc3.availableBalanceForReceive == a - p - fee) + + val (ac4, revocation2) = receiveCommit(ac3, commit2, alice.underlyingActor.nodeParams.keyManager) + assert(ac4.availableBalanceForSend == a - p - fee) + assert(ac4.availableBalanceForReceive == b) + + val (bc4, _) = receiveRevocation(bc3, revocation2) + assert(bc4.availableBalanceForSend == b) + assert(bc4.availableBalanceForReceive == a - p - fee) + + val cmdFail = CMD_FAIL_HTLC(0, Right(IncorrectOrUnknownPaymentDetails(p, 42))) + val (bc5, fail) = sendFail(bc4, cmdFail, bob.underlyingActor.nodeParams.privateKey) + assert(bc5.availableBalanceForSend == b) + assert(bc5.availableBalanceForReceive == a - p - fee) // a's balance won't return to previous before she acknowledges the fail + + val Right((ac5, _, _)) = receiveFail(ac4, fail) + assert(ac5.availableBalanceForSend == a - p - fee) + assert(ac5.availableBalanceForReceive == b) + + val (bc6, commit3) = sendCommit(bc5, bob.underlyingActor.nodeParams.keyManager) + assert(bc6.availableBalanceForSend == b) + assert(bc6.availableBalanceForReceive == a - p - fee) + + val (ac6, revocation3) = receiveCommit(ac5, commit3, alice.underlyingActor.nodeParams.keyManager) + assert(ac6.availableBalanceForSend == a) + assert(ac6.availableBalanceForReceive == b) + + val (bc7, _) = receiveRevocation(bc6, revocation3) + assert(bc7.availableBalanceForSend == b) + assert(bc7.availableBalanceForReceive == a) + + val (ac7, commit4) = sendCommit(ac6, alice.underlyingActor.nodeParams.keyManager) + assert(ac7.availableBalanceForSend == a) + assert(ac7.availableBalanceForReceive == b) + + val (bc8, revocation4) = receiveCommit(bc7, commit4, bob.underlyingActor.nodeParams.keyManager) + assert(bc8.availableBalanceForSend == b) + assert(bc8.availableBalanceForReceive == a) + + val (ac8, _) = receiveRevocation(ac7, revocation4) + assert(ac8.availableBalanceForSend == a) + assert(ac8.availableBalanceForReceive == b) + } + + test("correct values for availableForSend/availableForReceive (multiple htlcs)") { f => + import f._ + + val fee = 1720000 msat // fee due to the additional htlc output + val a = (772760000 msat) - fee // initial balance alice + val b = 190000000 msat // initial balance bob + val p1 = 10000000 msat // a->b payment + val p2 = 20000000 msat // a->b payment + val p3 = 40000000 msat // b->a payment + + val ac0 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + + assert(ac0.availableBalanceForSend > (p1 + p2)) // alice can afford the payments + assert(bc0.availableBalanceForSend > p3) // bob can afford the payment + assert(ac0.availableBalanceForSend == a) + assert(ac0.availableBalanceForReceive == b) + assert(bc0.availableBalanceForSend == b) + assert(bc0.availableBalanceForReceive == a) + + val (payment_preimage1, cmdAdd1) = makeCmdAdd(p1, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((ac1, add1)) = sendAdd(ac0, cmdAdd1, Local(UUID.randomUUID, None), currentBlockHeight) + assert(ac1.availableBalanceForSend == a - p1 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) + assert(ac1.availableBalanceForReceive == b) + + val (_, cmdAdd2) = makeCmdAdd(p2, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((ac2, add2)) = sendAdd(ac1, cmdAdd2, Local(UUID.randomUUID, None), currentBlockHeight) + assert(ac2.availableBalanceForSend == a - p1 - fee - p2 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) + assert(ac2.availableBalanceForReceive == b) + + val (payment_preimage3, cmdAdd3) = makeCmdAdd(p3, alice.underlyingActor.nodeParams.nodeId, currentBlockHeight) + val Right((bc1, add3)) = sendAdd(bc0, cmdAdd3, Local(UUID.randomUUID, None), currentBlockHeight) + assert(bc1.availableBalanceForSend == b - p3) // bob doesn't pay the fee + assert(bc1.availableBalanceForReceive == a) + + val bc2 = receiveAdd(bc1, add1) + assert(bc2.availableBalanceForSend == b - p3) + assert(bc2.availableBalanceForReceive == a - p1 - fee) + + val bc3 = receiveAdd(bc2, add2) + assert(bc3.availableBalanceForSend == b - p3) + assert(bc3.availableBalanceForReceive == a - p1 - fee - p2 - fee) + + val ac3 = receiveAdd(ac2, add3) + assert(ac3.availableBalanceForSend == a - p1 - fee - p2 - fee) + assert(ac3.availableBalanceForReceive == b - p3) + + val (ac4, commit1) = sendCommit(ac3, alice.underlyingActor.nodeParams.keyManager) + assert(ac4.availableBalanceForSend == a - p1 - fee - p2 - fee) + assert(ac4.availableBalanceForReceive == b - p3) + + val (bc4, revocation1) = receiveCommit(bc3, commit1, bob.underlyingActor.nodeParams.keyManager) + assert(bc4.availableBalanceForSend == b - p3) + assert(bc4.availableBalanceForReceive == a - p1 - fee - p2 - fee) + + val (ac5, _) = receiveRevocation(ac4, revocation1) + assert(ac5.availableBalanceForSend == a - p1 - fee - p2 - fee) + assert(ac5.availableBalanceForReceive == b - p3) + + val (bc5, commit2) = sendCommit(bc4, bob.underlyingActor.nodeParams.keyManager) + assert(bc5.availableBalanceForSend == b - p3) + assert(bc5.availableBalanceForReceive == a - p1 - fee - p2 - fee) + + val (ac6, revocation2) = receiveCommit(ac5, commit2, alice.underlyingActor.nodeParams.keyManager) + assert(ac6.availableBalanceForSend == a - p1 - fee - p2 - fee - fee) // alice has acknowledged b's hltc so it needs to pay the fee for it + assert(ac6.availableBalanceForReceive == b - p3) + + val (bc6, _) = receiveRevocation(bc5, revocation2) + assert(bc6.availableBalanceForSend == b - p3) + assert(bc6.availableBalanceForReceive == a - p1 - fee - p2 - fee - fee) + + val (ac7, commit3) = sendCommit(ac6, alice.underlyingActor.nodeParams.keyManager) + assert(ac7.availableBalanceForSend == a - p1 - fee - p2 - fee - fee) + assert(ac7.availableBalanceForReceive == b - p3) + + val (bc7, revocation3) = receiveCommit(bc6, commit3, bob.underlyingActor.nodeParams.keyManager) + assert(bc7.availableBalanceForSend == b - p3) + assert(bc7.availableBalanceForReceive == a - p1 - fee - p2 - fee - fee) + + val (ac8, _) = receiveRevocation(ac7, revocation3) + assert(ac8.availableBalanceForSend == a - p1 - fee - p2 - fee - fee) + assert(ac8.availableBalanceForReceive == b - p3) + + val cmdFulfill1 = CMD_FULFILL_HTLC(0, payment_preimage1) + val (bc8, fulfill1) = sendFulfill(bc7, cmdFulfill1) + assert(bc8.availableBalanceForSend == b + p1 - p3) // as soon as we have the fulfill, the balance increases + assert(bc8.availableBalanceForReceive == a - p1 - fee - p2 - fee - fee) + + val cmdFail2 = CMD_FAIL_HTLC(1, Right(IncorrectOrUnknownPaymentDetails(p2, 42))) + val (bc9, fail2) = sendFail(bc8, cmdFail2, bob.underlyingActor.nodeParams.privateKey) + assert(bc9.availableBalanceForSend == b + p1 - p3) + assert(bc9.availableBalanceForReceive == a - p1 - fee - p2 - fee - fee) // a's balance won't return to previous before she acknowledges the fail + + val cmdFulfill3 = CMD_FULFILL_HTLC(0, payment_preimage3) + val (ac9, fulfill3) = sendFulfill(ac8, cmdFulfill3) + assert(ac9.availableBalanceForSend == a - p1 - fee - p2 - fee + p3) // as soon as we have the fulfill, the balance increases + assert(ac9.availableBalanceForReceive == b - p3) + + val Right((ac10, _, _)) = receiveFulfill(ac9, fulfill1) + assert(ac10.availableBalanceForSend == a - p1 - fee - p2 - fee + p3) + assert(ac10.availableBalanceForReceive == b + p1 - p3) + + val Right((ac11, _, _)) = receiveFail(ac10, fail2) + assert(ac11.availableBalanceForSend == a - p1 - fee - p2 - fee + p3) + assert(ac11.availableBalanceForReceive == b + p1 - p3) + + val Right((bc10, _, _)) = receiveFulfill(bc9, fulfill3) + assert(bc10.availableBalanceForSend == b + p1 - p3) + assert(bc10.availableBalanceForReceive == a - p1 - fee - p2 - fee + p3) // the fee for p3 disappears + + val (ac12, commit4) = sendCommit(ac11, alice.underlyingActor.nodeParams.keyManager) + assert(ac12.availableBalanceForSend == a - p1 - fee - p2 - fee + p3) + assert(ac12.availableBalanceForReceive == b + p1 - p3) + + val (bc11, revocation4) = receiveCommit(bc10, commit4, bob.underlyingActor.nodeParams.keyManager) + assert(bc11.availableBalanceForSend == b + p1 - p3) + assert(bc11.availableBalanceForReceive == a - p1 - fee - p2 - fee + p3) + + val (ac13, _) = receiveRevocation(ac12, revocation4) + assert(ac13.availableBalanceForSend == a - p1 - fee - p2 - fee + p3) + assert(ac13.availableBalanceForReceive == b + p1 - p3) + + val (bc12, commit5) = sendCommit(bc11, bob.underlyingActor.nodeParams.keyManager) + assert(bc12.availableBalanceForSend == b + p1 - p3) + assert(bc12.availableBalanceForReceive == a - p1 - fee - p2 - fee + p3) + + val (ac14, revocation5) = receiveCommit(ac13, commit5, alice.underlyingActor.nodeParams.keyManager) + assert(ac14.availableBalanceForSend == a - p1 + p3) + assert(ac14.availableBalanceForReceive == b + p1 - p3) + + val (bc13, _) = receiveRevocation(bc12, revocation5) + assert(bc13.availableBalanceForSend == b + p1 - p3) + assert(bc13.availableBalanceForReceive == a - p1 + p3) + + val (ac15, commit6) = sendCommit(ac14, alice.underlyingActor.nodeParams.keyManager) + assert(ac15.availableBalanceForSend == a - p1 + p3) + assert(ac15.availableBalanceForReceive == b + p1 - p3) + + val (bc14, revocation6) = receiveCommit(bc13, commit6, bob.underlyingActor.nodeParams.keyManager) + assert(bc14.availableBalanceForSend == b + p1 - p3) + assert(bc14.availableBalanceForReceive == a - p1 + p3) + + val (ac16, _) = receiveRevocation(ac15, revocation6) + assert(ac16.availableBalanceForSend == a - p1 + p3) + assert(ac16.availableBalanceForReceive == b + p1 - p3) + } + +} 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 d44d98dc94..f486a370fa 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 @@ -21,8 +21,8 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -30,9 +30,10 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop +import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ import grizzled.slf4j.Logging -import org.scalatest.Tag +import org.scalatest.{Outcome, Tag} import scala.collection.immutable.Nil import scala.concurrent.duration._ @@ -46,7 +47,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], pipe: ActorRef, relayerA: ActorRef, relayerB: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) - override def withFixture(test: OneArgTest) = { + override def withFixture(test: OneArgTest): Outcome = { val fuzzy = test.tags.contains("fuzzy") val pipe = system.actorOf(Props(new FuzzyPipe(fuzzy))) val alice2blockchain = TestProbe() @@ -67,7 +68,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi relayerA ! alice relayerB ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) alice2blockchain.expectMsgType[WatchSpent] @@ -80,106 +81,99 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42, fundingTx) alice2blockchain.expectMsgType[WatchLost] bob2blockchain.expectMsgType[WatchLost] - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) + awaitCond(alice.stateName == NORMAL, 1 minute) + awaitCond(bob.stateName == NORMAL, 1 minute) } withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))) } - class SenderActor(channel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef, latch: CountDownLatch) extends Actor with ActorLogging { + class SenderActor(sendChannel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef, latch: CountDownLatch, count: Int) extends Actor with ActorLogging { // we don't want to be below htlcMinimumMsat - val requiredAmount = 1000000 + val requiredAmount = 1000000 msat def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey) = { // allow overpaying (no more than 2 times the required amount) - val amount = requiredAmount + Random.nextInt(requiredAmount) - val expiry = Globals.blockCount.get().toInt + Channel.MIN_CLTV_EXPIRY + 1 - PaymentLifecycle.buildCommand(UUID.randomUUID(), amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 + val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat + val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(blockHeight = 400000) + PaymentLifecycle.buildCommand(UUID.randomUUID(), paymentHash, Hop(null, dest, null) :: Nil, FinalLegacyPayload(amount, expiry))._1 } - def initiatePayment(stopping: Boolean) = - if (stopping) { - context stop self + def initiatePaymentOrStop(remaining: Int): Unit = + if (remaining > 0) { + paymentHandler ! ReceivePayment(Some(requiredAmount), "One coffee") + context become { + case req: PaymentRequest => + sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId) + context become { + case u: UpdateFulfillHtlc => + log.info(s"successfully sent htlc #${u.id}") + initiatePaymentOrStop(remaining - 1) + case u: UpdateFailHtlc => + log.warning(s"htlc failed: ${u.id}") + initiatePaymentOrStop(remaining - 1) + case Status.Failure(t) => + log.error(s"htlc error: ${t.getMessage}") + initiatePaymentOrStop(remaining - 1) + } + } } else { - paymentHandler ! ReceivePayment(Some(MilliSatoshi(requiredAmount)), "One coffee") - context become waitingForPaymentRequest + context stop self + latch.countDown() } - initiatePayment(false) + initiatePaymentOrStop(count) override def receive: Receive = ??? - def waitingForPaymentRequest: Receive = { - case req: PaymentRequest => - channel ! buildCmdAdd(req.paymentHash, req.nodeId) - context become waitingForFulfill(false) - } - - def waitingForFulfill(stopping: Boolean): Receive = { - case u: UpdateFulfillHtlc => - log.info(s"successfully sent htlc #${u.id}") - latch.countDown() - initiatePayment(stopping) - case u: UpdateFailHtlc => - log.warning(s"htlc failed: ${u.id}") - initiatePayment(stopping) - case Status.Failure(t) => - log.error(s"htlc error: ${t.getMessage}") - initiatePayment(stopping) - case 'stop => - log.warning(s"stopping...") - context become waitingForFulfill(true) - } - } test("fuzzy test with only one party sending HTLCs", Tag("fuzzy")) { f => import f._ - val latch = new CountDownLatch(100) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) + val senders = 2 + val totalMessages = 100 + val latch = new CountDownLatch(senders) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch, totalMessages / senders))) awaitCond(latch.getCount == 0, max = 2 minutes) - assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) - assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) + assert(List(NORMAL, OFFLINE, SYNCING).contains(alice.stateName)) + assert(List(NORMAL, OFFLINE, SYNCING).contains(bob.stateName)) } test("fuzzy test with both parties sending HTLCs", Tag("fuzzy")) { f => import f._ - val latch = new CountDownLatch(100) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) + val senders = 2 + val totalMessages = 100 + val latch = new CountDownLatch(2 * senders) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch, totalMessages / senders))) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch, totalMessages / senders))) awaitCond(latch.getCount == 0, max = 2 minutes) - assert(alice.stateName == NORMAL || alice.stateName == OFFLINE) - assert(bob.stateName == NORMAL || alice.stateName == OFFLINE) + assert(List(NORMAL, OFFLINE, SYNCING).contains(alice.stateName)) + assert(List(NORMAL, OFFLINE, SYNCING).contains(bob.stateName)) } - test("one party sends lots of htlcs send shutdown") { f => + test("one party sends lots of htlcs then shutdown") { f => import f._ - val latch = new CountDownLatch(20) - val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: Nil + val senders = 2 + val totalMessages = 20 + val latch = new CountDownLatch(senders) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch, totalMessages / senders))) awaitCond(latch.getCount == 0, max = 2 minutes) val sender = TestProbe() awaitCond({ sender.send(alice, CMD_CLOSE(None)) sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) == "ok" }, max = 30 seconds) - senders.foreach(_ ! 'stop) - awaitCond(alice.stateName == CLOSING) - awaitCond(alice.stateName == CLOSING) + awaitCond(alice.stateName == CLOSING, max = 3 minutes, interval = 1 second) + awaitCond(bob.stateName == CLOSING, max = 3 minutes, interval = 1 second) } - test("both parties send lots of htlcs send shutdown") { f => + test("both parties send lots of htlcs then shutdown") { f => import f._ - val latch = new CountDownLatch(30) - val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: - system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: Nil + val senders = 2 + val totalMessages = 100 + val latch = new CountDownLatch(2 * senders) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch, totalMessages / senders))) + for (_ <- 0 until senders) system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch, totalMessages / senders))) awaitCond(latch.getCount == 0, max = 2 minutes) val sender = TestProbe() awaitCond({ @@ -190,9 +184,8 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi // we only need that one of them succeeds resa == "ok" || resb == "ok" }, max = 30 seconds) - senders.foreach(_ ! 'stop) - awaitCond(alice.stateName == CLOSING) - awaitCond(alice.stateName == CLOSING) + awaitCond(alice.stateName == CLOSING, max = 3 minutes, interval = 1 second) + awaitCond(bob.stateName == CLOSING, max = 3 minutes, interval = 1 second) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala new file mode 100644 index 0000000000..d10a18a266 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala @@ -0,0 +1,122 @@ +package fr.acinq.eclair.channel + +import akka.testkit.TestProbe +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin._ +import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.blockchain.WatchEventSpent +import fr.acinq.eclair.channel.states.StateTestsHelperMethods +import fr.acinq.eclair.crypto.{Generators, KeyManager} +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, InputInfo} +import fr.acinq.eclair.wire.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass, _} +import org.scalatest.Outcome + +import scala.concurrent.duration._ + +class RecoverySpec extends TestkitBaseClass with StateTestsHelperMethods { + + 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))) + } + import setup._ + within(30 seconds) { + reachNormal(setup) + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(setup)) + } + } + + def aliceInit = Init(TestConstants.Alice.nodeParams.globalFeatures, TestConstants.Alice.nodeParams.localFeatures) + + def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) + + 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 + // then we add an htlc and sign it + addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN) + sender.expectMsg("ok") + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // alice will receive neither the revocation nor the commit sig + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.expectMsgType[CommitSig] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // then we manually replace alice's state with an older one + alice.setState(OFFLINE, oldStateData) + + // then we reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + val ce = bob2alice.expectMsgType[ChannelReestablish] + + // alice then realizes it has an old state... + 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) + + // 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.publishableTxs.commitTx.tx + + // actual tests starts here: let's see what we can do with Bob's commit tx + sender.send(alice, WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)) + + // from Bob's commit tx we can extract both funding public keys + val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) + // from Bob's commit tx we can also extract our p2wpkh output + val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get + + val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) + + val keyManager = TestConstants.Alice.nodeParams.keyManager + + // find our funding pub key + val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { + pub => + val channelKeyPath = KeyManager.channelKeyPath(pub) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + localPubkey.hash160 == pubKeyHash + } get + + // compute our to-remote pubkey + val channelKeyPath = KeyManager.channelKeyPath(fundingPubKey) + val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint.get) + + // spend our output + val tx = Transaction(version = 2, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey.publicKey)) :: Nil, + lockTime = 0) + + val sig = keyManager.sign( + ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), + keyManager.paymentPoint(channelKeyPath), + ce.myCurrentPerCommitmentPoint.get) + val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) + Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index ac21eeba31..9df32f77c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states import java.util.UUID import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ @@ -27,24 +28,28 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop +import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, NodeParams, TestConstants, randomBytes32} +import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} +import org.scalatest.{ParallelTestExecution, fixture} /** - * Created by PM on 23/08/2016. - */ -trait StateTestsHelperMethods extends TestKitBase { + * Created by PM on 23/08/2016. + */ +trait StateTestsHelperMethods extends TestKitBase with fixture.TestSuite with ParallelTestExecution { case class SetupFixture(alice: TestFSMRef[State, Data, Channel], - bob: TestFSMRef[State, Data, Channel], - alice2bob: TestProbe, - bob2alice: TestProbe, - alice2blockchain: TestProbe, - bob2blockchain: TestProbe, - router: TestProbe, - relayerA: TestProbe, - relayerB: TestProbe, - channelUpdateListener: TestProbe) + bob: TestFSMRef[State, Data, Channel], + alice2bob: TestProbe, + bob2alice: TestProbe, + alice2blockchain: TestProbe, + bob2blockchain: TestProbe, + router: TestProbe, + relayerA: TestProbe, + relayerB: TestProbe, + channelUpdateListener: TestProbe) { + def currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight + } def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: EclairWallet = new TestWallet): SetupFixture = { val alice2bob = TestProbe() @@ -65,12 +70,13 @@ trait StateTestsHelperMethods extends TestKitBase { def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { import setup._ + val channelVersion = ChannelVersion.STANDARD val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty - val pushMsat = if (tags.contains("no_push_msat")) 0 else TestConstants.pushMsat + val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelVersion) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -98,28 +104,33 @@ trait StateTestsHelperMethods extends TestKitBase { bob2blockchain.expectMsgType[WatchConfirmed] // deeply buried awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSendMsat == math.max(pushMsat - TestConstants.Alice.channelParams.channelReserveSatoshis * 1000, 0)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.channelReserve).max(0 msat)) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] } - def addHtlc(amountMsat: Int, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (ByteVector32, UpdateAddHtlc) = { - val R: ByteVector32 = randomBytes32 - val H: ByteVector32 = Crypto.sha256(R) + def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: Long): (ByteVector32, CMD_ADD_HTLC) = { + val payment_preimage: ByteVector32 = randomBytes32 + val payment_hash: ByteVector32 = Crypto.sha256(payment_preimage) + val expiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) + val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, payment_hash, Hop(null, destination, null) :: Nil, FinalLegacyPayload(amount, expiry))._1.copy(commit = false) + (payment_preimage, cmd) + } + + def addHtlc(amount: MilliSatoshi, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (ByteVector32, UpdateAddHtlc) = { val sender = TestProbe() - val receiverPubkey = r.underlyingActor.nodeParams.nodeId - val expiry = 400144 - val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) + val currentBlockHeight = s.underlyingActor.nodeParams.currentBlockHeight + val (payment_preimage, cmd) = makeCmdAdd(amount, r.underlyingActor.nodeParams.nodeId, currentBlockHeight) sender.send(s, cmd) sender.expectMsg("ok") val htlc = s2r.expectMsgType[UpdateAddHtlc] s2r.forward(r) awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(htlc)) - (R, htlc) + (payment_preimage, htlc) } - def fulfillHtlc(id: Long, R: ByteVector32, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe) = { + def fulfillHtlc(id: Long, R: ByteVector32, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { val sender = TestProbe() sender.send(s, CMD_FULFILL_HTLC(id, R)) sender.expectMsg("ok") @@ -128,7 +139,7 @@ trait StateTestsHelperMethods extends TestKitBase { awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fulfill)) } - def crossSign(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe) = { + def crossSign(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, 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 @@ -163,14 +174,18 @@ trait StateTestsHelperMethods extends TestKitBase { def channelId(a: TestFSMRef[State, Data, Channel]) = Helpers.getChannelId(a.stateData) + implicit class ChannelWithTestFeeConf(a: TestFSMRef[State, Data, Channel]) { def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets } implicit class PeerWithTestFeeConf(a: TestFSMRef[Peer.State, Peer.Data, Peer]) { def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets } + } 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 b71fd9de45..1b102e748d 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 @@ -24,16 +24,16 @@ import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _} import fr.acinq.eclair.wire.{AcceptChannel, Error, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector -import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ +import scala.concurrent.{Future, Promise} /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -41,7 +41,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp override def withFixture(test: OneArgTest): Outcome = { val noopWallet = new TestWallet { - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse].future // will never be completed + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse].future // will never be completed } val setup = if (test.tags.contains("mainnet")) { init(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), TestConstants.Bob.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash), wallet = noopWallet) @@ -49,11 +49,13 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp init(wallet = noopWallet) } import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val channelVersion = ChannelVersion.STANDARD + val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) + val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) + val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion) + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) @@ -83,7 +85,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // we don't want their dust limit to be below 546 - val lowDustLimitSatoshis = 545 + val lowDustLimitSatoshis = 545.sat alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage)) @@ -93,7 +95,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (to_self_delay too high)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - val delayTooHigh = 10000 + 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.maxToLocalDelayBlocks).getMessage)) @@ -104,7 +106,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong + 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)) @@ -114,7 +116,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp test("recv AcceptChannel (reserve below dust limit)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - val reserveTooSmall = accept.dustLimitSatoshis - 1 + 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)) @@ -125,7 +127,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val reserveTooSmall = open.dustLimitSatoshis - 1 + 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)) @@ -136,7 +138,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val open = alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].lastSent - val dustTooBig = open.channelReserveSatoshis + 1 + 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)) 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 6b74dfd654..7e3f85896e 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 @@ -22,14 +22,14 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{Error, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, ToMilliSatoshiConversion} import org.scalatest.Outcome import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -38,11 +38,13 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ - val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) - val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) + val channelVersion = ChannelVersion.STANDARD + val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) + val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures) + val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelVersion) + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain))) } @@ -69,20 +71,20 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (funding too low)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val lowFundingMsat = 100 - bob ! open.copy(fundingSatoshis = lowFundingMsat) + val lowFunding = 100.sat + bob ! open.copy(fundingSatoshis = lowFunding) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING).getMessage)) awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (funding too high)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val highFundingMsat = 100000000 + val highFundingMsat = 100000000.sat bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING_SATOSHIS).getMessage)) + assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Channel.MAX_FUNDING).getMessage).toAscii) awaitCond(bob.stateName == CLOSED) } @@ -99,17 +101,17 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (invalid push_msat)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val invalidPushMsat = 100000000000L + val invalidPushMsat = 100000000000L.msat bob ! open.copy(pushMsat = invalidPushMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (to_self_delay too high)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val delayTooHigh = 10000 + 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.maxToLocalDelayBlocks).getMessage)) @@ -120,7 +122,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper import f._ val open = alice2bob.expectMsgType[OpenChannel] // 30% is huge, recommended ratio is 1% - val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong + 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)) @@ -155,7 +157,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (reserve below dust)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val reserveTooSmall = open.dustLimitSatoshis - 1 + val reserveTooSmall = open.dustLimitSatoshis - 1.sat bob ! open.copy(channelReserveSatoshis = reserveTooSmall) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id @@ -166,12 +168,12 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper test("recv OpenChannel (toLocal + toRemote below reserve)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val fundingSatoshis = open.channelReserveSatoshis + 499 - val pushMsat = 500 * 1000 + val fundingSatoshis = open.channelReserveSatoshis + 499.sat + val pushMsat = (500 sat).toMilliSatoshi 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, 500 * 1000, (open.channelReserveSatoshis - 1) * 1000, open.channelReserveSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, pushMsat, (open.channelReserveSatoshis - 1.sat).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 2e3edfe5fa..3537d27ef8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -27,8 +27,8 @@ import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome import scodec.bits.ByteVector -import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ +import scala.concurrent.{Future, Promise} /** * Created by PM on 05/07/2016. @@ -47,7 +47,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 3bc38c87cf..93be60649f 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 @@ -17,14 +17,14 @@ package fr.acinq.eclair.channel.states.b import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{LongToBtcAmount, TestConstants, TestkitBaseClass, ToMilliSatoshiConversion} import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -41,14 +41,14 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel val setup = init() import setup._ val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) { - (1000100L, 1000000000L) // toRemote = 100 satoshis + (1000100 sat, (1000000 sat).toMilliSatoshi) // toLocal = 100 satoshis } else { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -71,13 +71,13 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f => import f._ - val fees = Transactions.commitWeight * TestConstants.feeratePerKw / 1000 - val reserve = Bob.channelParams.channelReserveSatoshis - val missing = 100 - fees - reserve + val fees = Satoshi(Transactions.commitWeight * TestConstants.feeratePerKw / 1000) + val reserve = Bob.channelParams.channelReserve + 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: missingSatoshis=${-1 * missing} reserveSatoshis=$reserve feesSatoshis=$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/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 12722b2e1b..fbe248ef21 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 @@ -26,7 +26,6 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{TestConstants, TestkitBaseClass} import org.scalatest.Outcome -import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -44,7 +43,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 d3ef649aab..5c54affbf6 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 @@ -18,22 +18,21 @@ package fr.acinq.eclair.channel.states.c import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction} -import fr.acinq.eclair.randomKey +import fr.acinq.bitcoin.{ByteVector32, Script, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{LongToBtcAmount, TestConstants, TestkitBaseClass, randomKey} import org.scalatest.Outcome import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -45,7 +44,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -94,7 +93,7 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH test("recv BITCOIN_FUNDING_DEPTHOK (bad funding amount)") { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - val badOutputAmount = fundingTx.txOut(0).copy(amount = Satoshi(1234567)) + val badOutputAmount = fundingTx.txOut(0).copy(amount = 1234567.sat) val badFundingTx = fundingTx.copy(txOut = Seq(badOutputAmount)) alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 42000, 42, badFundingTx) awaitCond(alice.stateName == CLOSED) 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 index de4fa0ea27..9068f0dec1 100644 --- 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 @@ -43,7 +43,7 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 4284a9c256..4ad7880959 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 @@ -22,14 +22,14 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} -import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} +import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.channel.{ChannelErrorOccured, _} +import fr.acinq.eclair.channel.{ChannelErrorOccurred, _} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ @@ -37,15 +37,15 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, htlcSuccessWeight, htlcTimeoutWeight, weight2fee} import fr.acinq.eclair.transactions.{IN, OUT, Transactions} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} +import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32, _} import org.scalatest.{Outcome, Tag} import scodec.bits._ import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -69,7 +69,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val add = CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -87,7 +87,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -99,8 +99,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) + val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000 msat, cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000.msat, h, originHtlc.cltvExpiry - CltvExpiryDelta(7), TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -117,11 +117,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val currentBlockCount = Globals.blockCount.get - val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val expiryTooSmall = CltvExpiry(currentBlockHeight + 3) + val add = CMD_ADD_HTLC(500000000 msat, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) + val error = ExpiryTooSmall(channelId(alice), Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(currentBlockHeight), expiryTooSmall, currentBlockHeight) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -130,11 +129,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val currentBlockCount = Globals.blockCount.get - val expiryTooBig = currentBlockCount + Channel.MAX_CLTV_EXPIRY + 1 - val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val expiryTooBig = (Channel.MAX_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight) + val add = CMD_ADD_HTLC(500000000 msat, randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) + val error = ExpiryTooBig(channelId(alice), maximum = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockCount = currentBlockHeight) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -143,40 +141,77 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = HtlcValueTooSmall(channelId(alice), 1000, 50) + val error = HtlcValueTooSmall(channelId(alice), 1000 msat, 50 msat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } + test("recv CMD_ADD_HTLC (increasing balance but still below reserve)", Tag("no_push_msat")) { f => + import f._ + val sender = TestProbe() + // channel starts with all funds on alice's side, alice sends some funds to bob, but not enough to make it go above reserve + val h = randomBytes32 + val add = CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + sender.send(alice, add) + sender.expectMsg("ok") + } + test("recv CMD_ADD_HTLC (insufficient funds)") { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(MilliSatoshi(Int.MaxValue), randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) + val error = InsufficientFunds(channelId(alice), amount = MilliSatoshi(Int.MaxValue), missing = 1376443 sat, reserve = 20000 sat, fees = 8960 sat) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) + } + + test("recv CMD_ADD_HTLC (insufficient funds, missing 1 msat)") { f => + import f._ + val sender = TestProbe() + val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] + val add = CMD_ADD_HTLC(initialState.commitments.availableBalanceForSend + 1.msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + sender.send(bob, add) + + val error = InsufficientFunds(channelId(alice), amount = add.amount, missing = 0 sat, reserve = 10000 sat, fees = 0 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } + test("recv CMD_ADD_HTLC (HTLC dips remote funder below reserve)") { f => + import f._ + val sender = TestProbe() + addHtlc(771000000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend === 40000.msat) + + // actual test begins + // at this point alice has the minimal amount to sustain a channel (29000 sat ~= alice reserve + commit fee) + val add = CMD_ADD_HTLC(120000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + sender.send(bob, add) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), add.amount, missing = 1680 sat, 10000 sat, 10680 sat) + sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate), Some(add)))) + } + test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f => import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(500000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(200000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(67600000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(1000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) + val error = InsufficientFunds(channelId(alice), amount = 1000000 msat, missing = 1000 sat, reserve = 20000 sat, fees = 12400 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -185,15 +220,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) - val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) + val error = InsufficientFunds(channelId(alice), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -202,9 +237,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(151000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(bob, add) - val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) + val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000 msat) sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) bob2alice.expectNoMsg(200 millis) } @@ -215,11 +250,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(10000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -230,7 +265,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -238,9 +273,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis.toMilliSatoshi * 2 / 3, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add2) - val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) + val error = InsufficientFunds(channelId(alice), add2.amount, 564013 sat, 20000 sat, 10680 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) alice2bob.expectNoMsg(200 millis) } @@ -255,7 +290,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) // actual test starts here - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -267,14 +302,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(500000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") // at the same time bob initiates a closing sender.send(bob, CMD_CLOSE(None)) sender.expectMsg("ok") // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(100000000, randomBytes32, 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(100000000 msat, randomBytes32, CltvExpiry(300000), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -288,7 +323,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) 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 @@ -298,7 +333,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (unexpected id)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) bob ! htlc.copy(id = 2) @@ -315,10 +350,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (value too small)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, TestConstants.emptyOnionPacket) + 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, actual = 150).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) @@ -330,10 +365,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket) + 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), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).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) @@ -345,12 +380,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) + 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), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).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) @@ -362,11 +397,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) + 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), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).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) @@ -378,9 +413,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (over max inflight htlc value)") { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + 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).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) @@ -394,9 +429,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) } - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + 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) awaitCond(bob.stateName == CLOSING) @@ -410,7 +445,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") val commitSig = alice2bob.expectMsgType[CommitSig] @@ -421,7 +456,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -453,34 +488,34 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() // for the test to be really useful we have constraint on parameters - assert(Alice.nodeParams.dustLimitSatoshis > Bob.nodeParams.dustLimitSatoshis) + assert(Alice.nodeParams.dustLimit > Bob.nodeParams.dustLimit) // 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.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight).toLong - val aliceMinOffer = Alice.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight).toLong - val bobMinReceive = Bob.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight).toLong - val bobMinOffer = Bob.nodeParams.dustLimitSatoshis + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight).toLong - val a2b_1 = bobMinReceive + 10 // will be in alice and bob tx - val a2b_2 = bobMinReceive + 20 // will be in alice and bob tx - val b2a_1 = aliceMinReceive + 10 // will be in alice and bob tx - val b2a_2 = bobMinOffer + 10 // will be only be in bob tx + val aliceMinReceive = Alice.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight) + val aliceMinOffer = Alice.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcTimeoutWeight) + val bobMinReceive = Bob.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, htlcSuccessWeight) + val bobMinOffer = Bob.nodeParams.dustLimit + weight2fee(TestConstants.feeratePerKw, 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 + val b2a_2 = bobMinOffer + 10.sat // will be only be in bob tx assert(a2b_1 > aliceMinOffer && a2b_1 > bobMinReceive) assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_1.toMilliSatoshi, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_2.toMilliSatoshi, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_1.toMilliSatoshi, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_2.toMilliSatoshi, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -500,11 +535,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose - val htlcCount = epsilons.size + val htlcCount = epsilons.size for (i <- epsilons) { - sender.send(alice, add.copy(amountMsat = add.amountMsat + i * 1000)) + sender.send(alice, add.copy(amount = add.amount + (i * 1000).msat)) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -532,7 +567,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (while waiting for RevokeAndAck (no pending changes)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -550,7 +585,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -560,7 +595,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(waitForRevocation.reSignAsap === false) // actual test starts here - val (r2, htlc2) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r2, htlc2) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectNoMsg(300 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation.copy(reSignAsap = true))) @@ -572,7 +607,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // channel starts with all funds on alice's side, so channel will be initially disabled on bob's side assert(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags) === false) // alice will send enough funds to bob to make it go above reserve - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) sender.expectMsg("ok") @@ -587,7 +622,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") bob2alice.expectMsgType[CommitSig] // it should update its channel_update - awaitCond(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags) == true) + awaitCond(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags)) // and broadcast it assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate === bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) } @@ -596,7 +631,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] sender.send(alice, CMD_SIGN) @@ -612,7 +647,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == IN)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.acked.size == 0) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.signed.size == 1) } @@ -621,7 +656,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] sender.send(alice, CMD_SIGN) @@ -637,26 +672,26 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc.id && h.direction == OUT)) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 1) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) } test("recv CommitSig (multiple htlcs in both directions)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r1, htlc1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r2, htlc2) = addHtlc(8000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + val (r3, htlc3) = addHtlc(300000 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r4, htlc4) = addHtlc(1000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r5, htlc5) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + val (r6, htlc6) = addHtlc(500000 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r7, htlc7) = addHtlc(4000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -698,12 +733,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val r = randomBytes32 val h = Crypto.sha256(r) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -714,8 +749,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { crossSign(alice, bob, alice2bob, bob2alice) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.exists(h => h.add.id == htlc1.id && h.direction == IN)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.htlcTxsAndSigs.size == 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocalMsat == initialState.commitments.localCommit.spec.toLocalMsat) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.count(_.amount == Satoshi(50000)) == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toLocal == initialState.commitments.localCommit.spec.toLocal) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2) } ignore("recv CommitSig (no changes)") { f => @@ -737,7 +772,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CommitSig (invalid signature)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // actual test begins @@ -754,7 +789,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx sender.send(alice, CMD_SIGN) @@ -775,7 +810,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx sender.send(alice, CMD_SIGN) @@ -792,11 +827,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2blockchain.expectMsgType[WatchConfirmed] } - test("recv RevokeAndAck (one htlc sent)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -814,7 +848,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (one htlc received)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -843,19 +877,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (multiple htlcs in both directions)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r1, htlc1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r2, htlc2) = addHtlc(8000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r2, htlc2) = addHtlc(8000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r3, htlc3) = addHtlc(300000, bob, alice, bob2alice, alice2bob) // b->a (dust) + val (r3, htlc3) = addHtlc(300000 msat, bob, alice, bob2alice, alice2bob) // b->a (dust) - val (r4, htlc4) = addHtlc(1000000, alice, bob, alice2bob, bob2alice) // a->b (regular) + val (r4, htlc4) = addHtlc(1000000 msat, alice, bob, alice2bob, bob2alice) // a->b (regular) - val (r5, htlc5) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r5, htlc5) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) - val (r6, htlc6) = addHtlc(500000, alice, bob, alice2bob, bob2alice) // a->b (dust) + val (r6, htlc6) = addHtlc(500000 msat, alice, bob, alice2bob, bob2alice) // a->b (dust) - val (r7, htlc7) = addHtlc(4000000, bob, alice, bob2alice, alice2bob) // b->a (regular) + val (r7, htlc7) = addHtlc(4000000 msat, bob, alice, bob2alice, alice2bob) // b->a (regular) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -880,13 +914,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (with reSignAsap=true)") { f => import f._ val sender = TestProbe() - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) - val (r2, htlc2) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r2, htlc2) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectNoMsg(300 millis) assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) @@ -901,7 +935,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -938,7 +972,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (forward UpdateFailHtlc)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) sender.expectMsg("ok") @@ -967,7 +1001,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") @@ -996,7 +1030,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv RevocationTimeout") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -1013,7 +1047,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FULFILL_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1040,7 +1074,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FULFILL_HTLC (invalid preimage)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1063,7 +1097,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r)) sender.expectMsg("ok") @@ -1083,7 +1117,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1117,7 +1151,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFulfillHtlc (invalid preimage)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) relayerB.expectMsgType[ForwardAdd] val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -1138,7 +1172,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1176,7 +1210,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_MALFORMED_HTLC") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1221,7 +1255,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailHtlc") { f => import f._ val sender = TestProbe() - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure))) sender.expectMsg("ok") @@ -1241,7 +1275,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() // Alice sends an HTLC to Bob, which they both sign - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // Bob fails the HTLC because he cannot parse it val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] @@ -1268,7 +1302,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1288,7 +1322,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateFailHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1319,6 +1353,20 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed] } + test("recv UpdateFailHtlc (invalid onion error length)") { f => + import f._ + val sender = TestProbe() + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + // Bob receives a failure with a completely invalid onion error (missing mac) + sender.send(bob, CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(260)(42)))) + sender.expectMsg("ok") + val fail = bob2alice.expectMsgType[UpdateFailHtlc] + assert(fail.id === htlc.id) + // We should rectify the packet length before forwarding upstream. + assert(fail.reason.length === Sphinx.FailurePacket.PacketLength) + } + test("recv CMD_UPDATE_FEE") { f => import f._ val sender = TestProbe() @@ -1396,7 +1444,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).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) @@ -1446,7 +1494,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { ignore("recv CMD_UPDATE_RELAY_FEE ") { f => import f._ val sender = TestProbe() - val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBaseMsat * 2 + val newFeeBaseMsat = TestConstants.Alice.nodeParams.feeBase * 2 val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.feeProportionalMillionth * 2 sender.send(alice, CMD_UPDATE_RELAY_FEE(newFeeBaseMsat, newFeeProportionalMillionth)) sender.expectMsg("ok") @@ -1471,7 +1519,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_CLOSE(None)) sender.expectMsg(Failure(CannotCloseWithUnsignedOutgoingHtlcs(channelId(bob)))) } @@ -1486,7 +1534,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (with signed sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_CLOSE(None)) sender.expectMsg("ok") @@ -1511,7 +1559,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_CLOSE (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1536,7 +1584,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with unacked sent htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] // actual test begins @@ -1557,7 +1605,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with unacked received htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // actual test begins sender.send(bob, Shutdown(ByteVector32.Zeroes, TestConstants.Alice.channelParams.defaultFinalScriptPubKey)) bob2alice.expectMsgType[Error] @@ -1581,7 +1629,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] @@ -1597,7 +1645,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (with signed htlcs)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1609,7 +1657,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Shutdown (while waiting for a RevokeAndAck)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -1628,14 +1676,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, CMD_CLOSE(None)) bob2alice.expectMsgType[Shutdown] // this is just so we have something to sign - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // now we can sign sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // adding an outgoing pending htlc - val (r1, htlc1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r1, htlc1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) // actual test begins // alice eventually gets bob's shutdown bob2alice.forward(alice) @@ -1665,7 +1713,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (no htlc timed out)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1677,7 +1725,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (an htlc timed out)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // actual test begins @@ -1696,11 +1744,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled signed htlc ignored by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill @@ -1714,9 +1762,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateFulfillHtlc] - sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong)) - val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) @@ -1731,11 +1779,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled proposed htlc ignored by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill but doesn't sign @@ -1749,9 +1797,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r, commit = false)) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateFulfillHtlc] - sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong)) - val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) @@ -1766,11 +1814,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CurrentBlockCount (fulfilled proposed htlc acked but not committed by upstream peer)") { f => import f._ val sender = TestProbe() - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill @@ -1789,9 +1837,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - Bob.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong)) - val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) @@ -1844,11 +1892,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -1880,7 +1928,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { claimHtlcTx.txOut(0).amount }).sum // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) - assert(amountClaimed == Satoshi(814880)) + assert(amountClaimed === 814880.sat) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimMain)) // claim-main @@ -1896,8 +1944,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // assert the feerate of the claim main is what we expect val expectedFeeRate = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.claimMainBlockTarget) - val expectedFee = Transactions.weight2fee(expectedFeeRate, Transactions.claimP2WPKHOutputWeight).toLong - val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount.toLong).sum - claimMain.txOut.map(_.amount.toLong).sum + val expectedFee = Transactions.weight2fee(expectedFeeRate, Transactions.claimP2WPKHOutputWeight) + val claimFee = claimMain.txIn.map(in => bobCommitTx.txOut(in.outPoint.index.toInt).amount).sum - claimMain.txOut.map(_.amount).sum assert(claimFee == expectedFee) } @@ -1905,11 +1953,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -1948,7 +1996,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { claimHtlcTx.txOut(0).amount }).sum // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) - assert(amountClaimed == Satoshi(822310)) + assert(amountClaimed === 822310.sat) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) // claim-main @@ -1971,7 +2019,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // bob = 200 000 def send(): Transaction = { // alice sends 8 000 sat - val (r, htlc) = addHtlc(10000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx @@ -2010,16 +2058,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { 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(0).amount == Satoshi(741500)) - assert(mainPenaltyTx.txOut(0).amount == Satoshi(195160)) - assert(htlcPenaltyTxs(0).txOut(0).amount == Satoshi(4540)) - assert(htlcPenaltyTxs(1).txOut(0).amount == Satoshi(4540)) - assert(htlcPenaltyTxs(2).txOut(0).amount == Satoshi(4540)) - assert(htlcPenaltyTxs(3).txOut(0).amount == Satoshi(4540)) + assert(mainTx.txOut(0).amount === 741500.sat) + assert(mainPenaltyTx.txOut(0).amount === 195160.sat) + assert(htlcPenaltyTxs(0).txOut(0).amount === 4540.sat) + assert(htlcPenaltyTxs(1).txOut(0).amount === 4540.sat) + assert(htlcPenaltyTxs(2).txOut(0).amount === 4540.sat) + assert(htlcPenaltyTxs(3).txOut(0).amount === 4540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - } test("recv BITCOIN_FUNDING_SPENT (revoked commit with identical htlcs)") { f => @@ -2030,7 +2077,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -2078,11 +2125,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv Error") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000, bob, alice, bob2alice, alice2bob) + val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) + val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(1, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(0, rb1, alice, bob, alice2bob, bob2alice) @@ -2218,7 +2265,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, 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.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here bob2alice.forward(alice) awaitCond({ @@ -2237,7 +2284,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10, 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.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) + val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.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)) @@ -2298,9 +2345,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42, null)) val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(Announcements.isEnabled(update1a.channelFlags) == true) - val (_, htlc1) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (_, htlc2) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + assert(Announcements.isEnabled(update1a.channelFlags)) + val (_, htlc1) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + val (_, htlc2) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.localChanges.proposed.size == 2) @@ -2311,7 +2358,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(relayerA.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc2.paymentHash) val update2a = alice2bob.expectMsgType[ChannelUpdate] assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate === update2a) - assert(Announcements.isEnabled(update2a.channelFlags) == false) + assert(!Announcements.isEnabled(update2a.channelFlags)) awaitCond(alice.stateName == OFFLINE) } @@ -2342,9 +2389,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2bob.forward(bob) val update1a = channelUpdateListener.expectMsgType[LocalChannelUpdate] val update1b = channelUpdateListener.expectMsgType[LocalChannelUpdate] - assert(Announcements.isEnabled(update1a.channelUpdate.channelFlags) == true) - val (_, htlc1) = addHtlc(10000, alice, bob, alice2bob, bob2alice) - val (_, htlc2) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + assert(Announcements.isEnabled(update1a.channelUpdate.channelFlags)) + val (_, htlc1) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + val (_, htlc2) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.localChanges.proposed.size == 2) @@ -2355,7 +2402,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(relayerA.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc2.paymentHash) val update2a = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(update1a.channelUpdate.timestamp < update2a.channelUpdate.timestamp) - assert(Announcements.isEnabled(update2a.channelUpdate.channelFlags) == false) + assert(!Announcements.isEnabled(update2a.channelUpdate.channelFlags)) awaitCond(alice.stateName == OFFLINE) } 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 866a3346f6..528e384104 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 @@ -22,7 +22,9 @@ import akka.actor.Status import akka.testkit.{TestActorRef, TestProbe} import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} -import fr.acinq.eclair.blockchain.{CurrentBlockCount, PublishAsap, WatchConfirmed, WatchEventSpent} +import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.Channel.LocalError import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods @@ -31,21 +33,24 @@ import fr.acinq.eclair.payment.CommandBuffer.CommandSend import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32} -import org.scalatest.Outcome +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32} +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { type FixtureParam = SetupFixture override def withFixture(test: OneArgTest): Outcome = { - val setup = init() + 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))) + } import setup._ within(30 seconds) { reachNormal(setup) @@ -60,13 +65,13 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { def bobInit = Init(TestConstants.Bob.nodeParams.globalFeatures, TestConstants.Bob.nodeParams.localFeatures) /** - * This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig - */ + * This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig + */ test("re-send update+sig after first commitment") { f => import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000 msat, ByteVector32.Zeroes, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob) @@ -85,8 +90,12 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( + TestConstants.Bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), + bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( + TestConstants.Alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), + aliceCommitments.localCommit.index) // a didn't receive any update or sig @@ -137,13 +146,13 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } /** - * This test checks the case where a disconnection occurs *right after* the counterparty receives a new sig - */ + * This test checks the case where a disconnection occurs *right after* the counterparty receives a new sig + */ test("re-send lost revocation") { f => import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob, ab_add_0) @@ -169,8 +178,12 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments - val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint(bobCommitments.localParams.channelKeyPath, bobCommitments.localCommit.index) - val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint(aliceCommitments.localParams.channelKeyPath, aliceCommitments.localCommit.index) + val bobCurrentPerCommitmentPoint = TestConstants.Bob.keyManager.commitmentPoint( + TestConstants.Bob.keyManager.channelKeyPath(bobCommitments.localParams, bobCommitments.channelVersion), + bobCommitments.localCommit.index) + val aliceCurrentPerCommitmentPoint = TestConstants.Alice.keyManager.commitmentPoint( + TestConstants.Alice.keyManager.channelKeyPath(aliceCommitments.localParams, aliceCommitments.channelVersion), + aliceCommitments.localCommit.index) // a didn't receive the sig val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0, Some(PrivateKey(ByteVector32.Zeroes)), Some(aliceCurrentPerCommitmentPoint))) @@ -204,11 +217,11 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - val (ra1, htlca1) = addHtlc(250000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000, alice, bob, alice2bob, bob2alice) + val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val oldStateData = alice.stateData fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) @@ -261,7 +274,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // we start by storing the current state val oldStateData = alice.stateData // then we add an htlc and sign it - addHtlc(250000000, alice, bob, alice2bob, bob2alice) + addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] @@ -347,7 +360,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we make alice update here relay fee - sender.send(alice, CMD_UPDATE_RELAY_FEE(4200, 123456)) + sender.send(alice, CMD_UPDATE_RELAY_FEE(4200 msat, 123456)) sender.expectMsg("ok") // alice doesn't broadcast the new channel_update yet @@ -365,7 +378,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // 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) + assert(channelUpdate.feeBaseMsat === 4200.msat) assert(channelUpdate.feeProportionalMillionths === 123456) assert(Announcements.isEnabled(channelUpdate.channelFlags)) @@ -387,7 +400,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we attempt to send a payment - sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(4200 msat, randomBytes32, CltvExpiry(123456), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val failure = sender.expectMsgType[Status.Failure] val AddHtlcFailed(_, _, ChannelUnavailable(_), _, _, _) = failure.cause @@ -401,11 +414,11 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val register = TestProbe() val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) - val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccured]) + system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val initialCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx @@ -419,9 +432,9 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // We simulate a pending fulfill on that HTLC but not relayed. // When it is close to expiring upstream, we should close the channel. sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FULFILL_HTLC(htlc.id, r, commit = true))) - sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong)) - val ChannelErrorOccured(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccured] + val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcWillTimeoutUpstream]) @@ -442,7 +455,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val register = TestProbe() val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref)) - val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) sender.send(alice, INPUT_DISCONNECTED) @@ -452,11 +465,77 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // 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. - sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0))))) - sender.send(bob, CurrentBlockCount(htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks)) + sender.send(commandBuffer, CommandSend(htlc.channelId, htlc.id, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, 0))))) + sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong)) bob2blockchain.expectNoMsg(250 millis) alice2blockchain.expectNoMsg(250 millis) } + test("handle feerate changes while offline (funder scenario)") { f => + import f._ + val sender = TestProbe() + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] + val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx + + val localFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw + val tooHighFeeratePerKw = ((alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong + val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) + + // alice is funder + sender.send(alice, CurrentFeerates(highFeerate)) + alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) + } + + test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f => + import f._ + val sender = TestProbe() + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] + val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx + + val localFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw + val tooHighFeeratePerKw = ((alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong + val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) + + // this time Alice will ignore feerate changes for the offline channel + sender.send(alice, CurrentFeerates(highFeerate)) + alice2blockchain.expectNoMsg() + alice2bob.expectNoMsg() + } + + test("handle feerate changes while offline (fundee scenario)") { f => + import f._ + val sender = TestProbe() + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL] + val bobCommitTx = bobStateData.commitments.localCommit.publishableTxs.commitTx.tx + + val localFeeratePerKw = bobStateData.commitments.localCommit.spec.feeratePerKw + val tooHighFeeratePerKw = ((bob.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong + val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) + + // bob is fundee + sender.send(bob, CurrentFeerates(highFeerate)) + bob2blockchain.expectMsg(PublishAsap(bobCommitTx)) + } } 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 760efbe3f6..6dc7c367a1 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 @@ -21,24 +21,24 @@ import java.util.UUID import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} -import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Hop +import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} -import org.scalatest.{Outcome, Tag} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32} +import org.scalatest.Outcome import scodec.bits.ByteVector import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -55,9 +55,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() // alice sends an HTLC to bob val h1 = Crypto.sha256(r1) - val amount1 = 300000000 - val expiry1 = 400144 - val cmd1 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val amount1 = 300000000 msat + val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) + val cmd1 = PaymentLifecycle.buildCommand(UUID.randomUUID, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount1, expiry1))._1.copy(commit = false) sender.send(alice, cmd1) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -65,9 +65,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteChanges.proposed == htlc1 :: Nil) // alice sends another HTLC to bob val h2 = Crypto.sha256(r2) - val amount2 = 200000000 - val expiry2 = 400144 - val cmd2 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val amount2 = 200000000 msat + val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) + val cmd2 = PaymentLifecycle.buildCommand(UUID.randomUUID, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, FinalLegacyPayload(amount2, expiry2))._1.copy(commit = false) sender.send(alice, cmd2) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -104,7 +104,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000 msat, r1, cltvExpiry = CltvExpiry(300000), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -217,7 +217,6 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_HTLC (acknowledge in case of failure)") { f => import f._ val sender = TestProbe() - val r = randomBytes32 val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) // this will fail sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) @@ -240,7 +239,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, ByteVector32.Zeroes, FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, randomBytes32, FailureMessageCodecs.BADONION)) sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) assert(initialState == bob.stateData) } @@ -249,7 +248,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, ByteVector32.Zeroes, 42)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, randomBytes32, 42)) sender.expectMsg(Failure(InvalidFailureCode(channelId(bob)))) assert(initialState == bob.stateData) } @@ -257,10 +256,8 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_FAIL_MALFORMED_HTLC (acknowledge in case of failure)") { f => import f._ val sender = TestProbe() - val r = randomBytes32 val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] - - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, ByteVector32.Zeroes, FailureMessageCodecs.BADONION)) // this will fail + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, randomBytes32, FailureMessageCodecs.BADONION)) // this will fail sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42))) relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42)) } @@ -585,7 +582,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) sender.send(bob, fee) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missingSatoshis = 72120000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage) + assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx //bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees) @@ -638,7 +635,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[PublishAsap] // htlc timeout 2 alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 1 alice2blockchain.expectMsgType[PublishAsap] // htlc delayed 2 - val watch = alice2blockchain.expectMsgType[WatchConfirmed] + val watch = alice2blockchain.expectMsgType[WatchConfirmed] assert(watch.event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) } @@ -696,7 +693,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { claimHtlcTx.txOut(0).amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) - assert(amountClaimed == Satoshi(774040)) + assert(amountClaimed === 774040.sat) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) @@ -743,7 +740,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { claimHtlcTx.txOut(0).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 == Satoshi(481210)) + assert(amountClaimed === 481210.sat) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(bobCommitTx)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(claimTxes(0))) @@ -789,10 +786,10 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { 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(0).amount == Satoshi(284940)) - assert(mainPenaltyTx.txOut(0).amount == Satoshi(195160)) - assert(htlc1PenaltyTx.txOut(0).amount == Satoshi(194540)) - assert(htlc2PenaltyTx.txOut(0).amount == Satoshi(294540)) + assert(mainTx.txOut(0).amount === 284940.sat) + assert(mainPenaltyTx.txOut(0).amount === 195160.sat) + assert(htlc1PenaltyTx.txOut(0).amount === 194540.sat) + assert(htlc2PenaltyTx.txOut(0).amount === 294540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) 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 3b798fe887..ea25fabb63 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 @@ -21,16 +21,16 @@ import java.util.UUID import akka.actor.Status.Failure import akka.event.LoggingAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} -import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKw} +import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment.Local import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -38,8 +38,8 @@ import scala.concurrent.duration._ import scala.util.Success /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -85,7 +85,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(5000000000L msat, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = CltvExpiry(300000), onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -111,7 +111,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods private def testFeeConverge(f: FixtureParam) = { import f._ - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = 0.sat do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -135,9 +135,9 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx - sender.send(bob, aliceCloseSig.copy(feeSatoshis = 99000)) // 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")) + sender.send(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=Satoshi(99000)")) bob2blockchain.expectMsg(PublishAsap(tx)) bob2blockchain.expectMsgType[PublishAsap] bob2blockchain.expectMsgType[WatchConfirmed] @@ -158,7 +158,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { f => import f._ - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = 0.sat do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -193,7 +193,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods // at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING] implicit val log: LoggingAdapter = bob.underlyingActor.implicitLog - val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, Satoshi(aliceClose1.feeSatoshis), aliceClose1.signature) + val Success(bobClosingTx) = Closing.checkClosingSignature(Bob.keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, aliceClose1.feeSatoshis, aliceClose1.signature) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx) alice2blockchain.expectMsgType[PublishAsap] 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 e33e854396..c9b3628452 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 @@ -22,17 +22,16 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{ByteVector32, OutPoint, ScriptFlags, Transaction, TxIn} -import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} +import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratesPerKw} import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{Data, State, _} import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -40,8 +39,8 @@ import scala.compat.Platform import scala.concurrent.duration._ /** - * Created by PM on 05/07/2016. - */ + * Created by PM on 05/07/2016. + */ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { @@ -66,7 +65,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, bob2alice.ref, aliceInit) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -85,27 +84,27 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, Nil))) } } else { - within(30 seconds) { - reachNormal(setup) - val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield { - val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[ForwardAdd] - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs - fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) - // alice forwards the fulfill upstream - relayerA.expectMsgType[ForwardFulfill] - crossSign(bob, alice, bob2alice, alice2bob) - // bob confirms that it has forwarded the fulfill to alice - relayerB.expectMsgType[CommandBuffer.CommandAck] - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs - bobCommitTx1 :: bobCommitTx2 :: Nil - }).flatten - - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, bobCommitTxes))) - } + within(30 seconds) { + reachNormal(setup) + val bobCommitTxes: List[PublishableTxs] = (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[ForwardAdd] + val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs + fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) + // alice forwards the fulfill upstream + relayerA.expectMsgType[ForwardFulfill] + crossSign(bob, alice, bob2alice, alice2bob) + // bob confirms that it has forwarded the fulfill to alice + relayerB.expectMsgType[CommandBuffer.CommandAck] + val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs + bobCommitTx1 :: bobCommitTx2 :: Nil + }).flatten + + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, bobCommitTxes))) + } } } @@ -123,7 +122,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) // agreeing on a closing fee - var aliceCloseFee, bobCloseFee = 0L + var aliceCloseFee, bobCloseFee = 0.sat do { aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) @@ -156,7 +155,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val mutualClosingFeeRate = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.mutualCloseBlockTarget) val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) assert(alice.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == 250) - assert(closing.feeSatoshis == expectedFirstProposedFee.amount) + assert(closing.feeSatoshis == expectedFirstProposedFee) } test("recv BITCOIN_FUNDING_PUBLISH_FAILED", Tag("funding_unconfirmed")) { f => @@ -300,7 +299,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000 msat, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = CltvExpiry(300000), onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) @@ -379,7 +378,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv BITCOIN_OUTPUT_SPENT") { f => import f._ // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) relayerB.expectMsgType[ForwardAdd] // an error occurs and alice publishes her commit tx @@ -419,15 +418,15 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) // alice sends an htlc to bob - val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx - val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-delayed-output - val htlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-timeout - val claimDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-delayed-output + val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-delayed-output + val htlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-timeout + val claimDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-delayed-output assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx)) assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output @@ -438,7 +437,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 42, 0, aliceCommitTx) - assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay) + assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay.toInt) assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainDelayedTx), 200, 0, claimMainDelayedTx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcTimeoutTx), 201, 0, htlcTimeoutTx) @@ -453,7 +452,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // alice sends an htlc - val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -484,7 +483,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // alice sends an htlc - val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice) + val (r, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) sender.send(alice, CMD_SIGN) sender.expectMsg("ok") @@ -545,7 +544,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val oldStateData = alice.stateData - val (ra1, htlca1) = addHtlc(25000000, alice, bob, alice2bob, bob2alice) + val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) @@ -654,8 +653,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) // alice publishes and watches the penalty tx val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit alice2blockchain.expectMsgType[WatchConfirmed] // claim-main alice2blockchain.expectMsgType[WatchSpent] // main-penalty @@ -669,10 +668,10 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0, mainPenaltyTx) alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) // we published this alice2blockchain.expectMsgType[WatchConfirmed] // htlc-penalty - val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx + val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his HtlcSuccess tx alice2blockchain.expectMsgType[WatchConfirmed] // htlc-success - val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx + val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx), 0, 0, bobHtlcSuccessTx) // bob won alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTxs), 0, 0, claimHtlcDelayedPenaltyTxs) // bob won awaitCond(alice.stateName == CLOSED) @@ -686,8 +685,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) // alice publishes and watches the penalty tx val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit alice2blockchain.expectMsgType[WatchConfirmed] // claim-main alice2blockchain.expectMsgType[WatchSpent] // main-penalty diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala index 37a8dc367b..4cb96ea7b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/LocalKeyManagerSpec.scala @@ -16,9 +16,11 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} +import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.channel.ChannelVersion import org.scalatest.FunSuite import scodec.bits._ @@ -57,4 +59,86 @@ class LocalKeyManagerSpec extends FunSuite { assert(keyManager1.fundingPublicKey(keyPath) != keyManager2.fundingPublicKey(keyPath)) assert(keyManager1.commitmentPoint(keyPath, 1) != keyManager2.commitmentPoint(keyPath, 1)) } + + test("compute channel key path from funding keys") { + // if this test fails it means that we don't generate the same channel key path from the same funding pubkey, which + // will break existing channels ! + val pub = PrivateKey(ByteVector32.fromValidHex("01" * 32)).publicKey + val keyPath = KeyManager.channelKeyPath(pub) + assert(keyPath.toString() == "m/1909530642'/1080788911/847211985'/1791010671/1303008749'/34154019'/723973395/767609665") + } + + def makefundingKeyPath(entropy: ByteVector, isFunder: Boolean) = { + 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) + KeyPath(items :+ last) + } + + test("test vectors (testnet, funder)") { + val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isFunder = true) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"216414970b4216b197a1040367419ad6922f80e8b73ced083e9afe5e6ddd8e4c").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"a4e7ab3c54752a3487b3c474467843843f28d3bb9113e65e92056ad45d1e318e").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"de24c43d24b8d6bc66b020ac81164206bb577c7924511d4e99431c0d60505012").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"8aa7b8b14a7035540c331c030be0dd73e8806fb0c97a2519d63775c2f579a950").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"94eca6eade204d6e753344c347b46bb09067c92b2fe371cf4f8362c1594c8c59").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("64e9d1e9840add3bb02c1525995edd28feea67f1df7a9ee075179e8541adc7a2"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (testnet, fundee)") { + val seed = ByteVector.fromValidHex("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") + val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isFunder = false) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"7bb8019c99fcba1c6bd0cc7f3c635c14c658d26751232d6a6350d8b6127d53c3").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"26510db99546c9b08418fe9df2da710a92afa6cc4e5681141610dfb8019052e6").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"0766c93fd06f69287fcc7b343916e678b83942345d4080e83f4c8a061b1a9f4b").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"094aa052a9647228fd80e42461cae26c04f6cdd1665b816d4660df686915319a").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"8ec62bd03b241a2e522477ae1a9861a668429ab3e443abd2aa0f2f10e2dc2206").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("c49e98202b0fee19f28fd3af60691aaacdd2c09e20896f5fa3ad1b9b70e4879f"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (mainnet, funder)") { + val seed = ByteVector.fromValidHex("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isFunder = true) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"b97c04796850e9d74a06c9d7230d85e2ecca3598b162ddf902895ece820c8f09").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"ee13db7f2d7e672f21395111ee169af8462c6e8d1a6a78d808f7447b27155ffb").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"7fc18e4c925bf3c5a83411eac7f234f0c5eaef9a8022b22ec6e3272ae329e17e").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"c0d9a3e3601d79b11b948db9d672fcddafcb9a3c0873c6a738bb09087ea2bfc6").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"bd3ba7068d131a9ab47f33202d532c5824cc5fc35a9adada3644ac2994372228").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("7799de34239f97837a12191f5b60e766e32e9704bb84b0f12b539e9bf6a0dc2a"), 0xFFFFFFFFFFFFL)) + } + + test("test vectors (mainnet, fundee)") { + val seed = ByteVector.fromValidHex("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") + val keyManager = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash) + val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isFunder = false) + val fundingPub = keyManager.fundingPublicKey(fundingKeyPath) + + val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) + val channelKeyPath = keyManager.channelKeyPath(localParams, ChannelVersion.STANDARD) + + assert(fundingPub.publicKey == PrivateKey(hex"46a4e818615a48a99ce9f6bd73eea07d5822dcfcdff18081ea781d4e5e6c036c").publicKey) + assert(keyManager.revocationPoint(channelKeyPath).publicKey == PrivateKey(hex"c2cd9e2f9f8203f16b1751bd252285bb2e7fc4688857d620467b99645ebdfbe6").publicKey) + assert(keyManager.paymentPoint(channelKeyPath).publicKey == PrivateKey(hex"1e4d3527788b39dc8ebc0ae6368a67e92eff55a43bea8e93054338ca850fa340").publicKey) + assert(keyManager.delayedPaymentPoint(channelKeyPath).publicKey == PrivateKey(hex"6bc30b0852fbc653451662a1ff6ad530f311d58b5e5661b541eb57dba8206937").publicKey) + assert(keyManager.htlcPoint(channelKeyPath).publicKey == PrivateKey(hex"b1be27b5232e3bc5d6a261949b4ee68d96fa61f481998d36342e2ad99444cf8a").publicKey) + assert(keyManager.commitmentSecret(channelKeyPath, 0).value == ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("eeb3bad6808e8bb5f1774581ccf64aa265fef38eca80a1463d6310bb801b3ba7"), 0xFFFFFFFFFFFFL)) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala index bb4c480c32..3f2dd1ec1b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseDemo.scala @@ -25,7 +25,7 @@ import scodec.bits.{ByteVector, _} * Created by fabrice on 12/12/16. */ object NoiseDemo extends App { - implicit val system = ActorSystem("mySystem") + implicit val system = ActorSystem("test") class NoiseHandler(keyPair: KeyPair, rs: Option[ByteVector], them: ActorRef, isWriter: Boolean, listenerFactory: => ActorRef) extends Actor with Stash { // initiator must know pubkey (i.e long-term ID) of responder 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 59a622b48b..9f08344ecf 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 @@ -18,16 +18,16 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.wire import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{UInt64, wire} import org.scalatest.FunSuite import scodec.bits._ import scala.util.Success /** - * Created by fabrice on 10/01/17. - */ + * Created by fabrice on 10/01/17. + */ class SphinxSpec extends FunSuite { import Sphinx._ @@ -249,7 +249,7 @@ class SphinxSpec extends FunSuite { val packet = FailurePacket.wrap( FailurePacket.wrap( - FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(ByteVector32.Zeroes)), + FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(UInt64(0), 0)), sharedSecrets(1)), sharedSecrets(2)) @@ -299,6 +299,20 @@ class SphinxSpec extends FunSuite { } } + test("intermediate node replies with an invalid onion payload length") { + // The error will not be recoverable by the sender, but we must still forward it. + val sharedSecret = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") + val errors = Seq( + ByteVector.fill(FailurePacket.PacketLength - MacLength)(13), + ByteVector.fill(FailurePacket.PacketLength + MacLength)(13) + ) + + for (error <- errors) { + val wrapped = FailurePacket.wrap(error, sharedSecret) + assert(wrapped.length === FailurePacket.PacketLength) + } + } + test("intermediate node replies with a failure message (reference test vector)") { for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index 282841c3cc..7eb9b8d1b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -18,18 +18,18 @@ package fr.acinq.eclair.db import java.util.UUID -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} +import fr.acinq.bitcoin.Transaction +import fr.acinq.eclair._ import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError} -import fr.acinq.eclair.channel.{AvailableBalanceChanged, ChannelErrorOccured, NetworkFeePaid} +import fr.acinq.eclair.channel.{AvailableBalanceChanged, ChannelErrorOccurred, NetworkFeePaid} import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.{ChannelCodecs, ChannelCodecsSpec} -import fr.acinq.eclair._ import org.scalatest.FunSuite -import concurrent.duration._ import scala.compat.Platform +import scala.concurrent.duration._ class SqliteAuditDbSpec extends FunSuite { @@ -44,16 +44,21 @@ class SqliteAuditDbSpec extends FunSuite { val sqlite = TestConstants.sqliteInMemory() val db = new SqliteAuditDb(sqlite) - val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) - val e2 = PaymentReceived(MilliSatoshi(42000), randomBytes32, randomBytes32) - val e3 = PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) - val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") - val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) - val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = (Platform.currentTime.milliseconds + 10.minutes).toMillis) - val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelCodecsSpec.commitments) - val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000, true, false, "mutual") - val e9 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), true) - val e10 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), true) + val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, PaymentSent.PartialPayment(ChannelCodecs.UNKNOWN_UUID, 42000 msat, 1000 msat, randomBytes32, None) :: Nil) + val pp2a = PaymentReceived.PartialPayment(42000 msat, randomBytes32) + val pp2b = PaymentReceived.PartialPayment(42100 msat, randomBytes32) + val e2 = PaymentReceived(randomBytes32, pp2a :: pp2b :: Nil) + val e3 = PaymentRelayed(42000 msat, 1000 msat, randomBytes32, randomBytes32, randomBytes32) + val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), 42 sat, "mutual") + val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = 0) + val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32, None, timestamp = 1) + val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp5a :: pp5b :: Nil) + val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = (Platform.currentTime.milliseconds + 10.minutes).toMillis) + val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp6 :: Nil) + val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000 msat, ChannelCodecsSpec.commitments) + val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual") + val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) + val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true) db.add(e1) db.add(e2) @@ -66,12 +71,12 @@ class SqliteAuditDbSpec extends FunSuite { db.add(e9) db.add(e10) - assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toSeconds).toSet === Set(e1, e5, e6)) - assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toSeconds).toList === List(e1)) - assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toSeconds).toList === List(e2)) - assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toSeconds).toList === List(e3)) - assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toSeconds).size === 1) - assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toSeconds).head.txType === "mutual") + assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5.copy(id = pp5a.id, parts = pp5a :: Nil), e5.copy(id = pp5b.id, parts = pp5b :: Nil), e6.copy(id = pp6.id))) + assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1)) + assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2.copy(parts = pp2a :: Nil), e2.copy(parts = pp2b :: Nil))) + assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3)) + assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1) + assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual") } test("stats") { @@ -86,20 +91,20 @@ class SqliteAuditDbSpec extends FunSuite { val c2 = randomBytes32 val c3 = randomBytes32 - db.add(PaymentRelayed(MilliSatoshi(46000), MilliSatoshi(44000), randomBytes32, randomBytes32, c1)) - db.add(PaymentRelayed(MilliSatoshi(41000), MilliSatoshi(40000), randomBytes32, randomBytes32, c1)) - db.add(PaymentRelayed(MilliSatoshi(43000), MilliSatoshi(42000), randomBytes32, randomBytes32, c1)) - db.add(PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(40000), randomBytes32, randomBytes32, c2)) + db.add(PaymentRelayed(46000 msat, 44000 msat, randomBytes32, randomBytes32, c1)) + db.add(PaymentRelayed(41000 msat, 40000 msat, randomBytes32, randomBytes32, c1)) + db.add(PaymentRelayed(43000 msat, 42000 msat, randomBytes32, randomBytes32, c1)) + db.add(PaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomBytes32, c2)) - db.add(NetworkFeePaid(null, n1, c1, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(100), "funding")) - db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(200), "funding")) - db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(300), "mutual")) - db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(400), "funding")) + db.add(NetworkFeePaid(null, n1, c1, Transaction(0, Seq.empty, Seq.empty, 0), 100 sat, "funding")) + db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 200 sat, "funding")) + db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 300 sat, "mutual")) + db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), 400 sat, "funding")) assert(db.stats.toSet === Set( - Stats(channelId = c1, avgPaymentAmountSatoshi = 42, paymentCount = 3, relayFeeSatoshi = 4, networkFeeSatoshi = 100), - Stats(channelId = c2, avgPaymentAmountSatoshi = 40, paymentCount = 1, relayFeeSatoshi = 2, networkFeeSatoshi = 500), - Stats(channelId = c3, avgPaymentAmountSatoshi = 0, paymentCount = 0, relayFeeSatoshi = 0, networkFeeSatoshi = 400) + Stats(channelId = c1, avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 100 sat), + Stats(channelId = c2, avgPaymentAmount = 40 sat, paymentCount = 1, relayFee = 2 sat, networkFee = 500 sat), + Stats(channelId = c3, avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat) )) } @@ -129,11 +134,12 @@ class SqliteAuditDbSpec extends FunSuite { assert(getVersion(statement, "audit", 3) == 1) // we expect version 1 } - val ps = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) - val ps1 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42001), MilliSatoshi(1001), randomBytes32, randomBytes32, randomBytes32) - val ps2 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42002), MilliSatoshi(1002), randomBytes32, randomBytes32, randomBytes32) - val e1 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), true) - val e2 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), true) + val ps = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None) :: Nil) + 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, 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(wire.Error(randomBytes32, "remote oops")), isFatal = true) // add a row (no ID on sent) using(connection.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?)")) { statement => @@ -141,7 +147,7 @@ class SqliteAuditDbSpec extends FunSuite { statement.setLong(2, ps.feesPaid.toLong) statement.setBytes(3, ps.paymentHash.toArray) statement.setBytes(4, ps.paymentPreimage.toArray) - statement.setBytes(5, ps.toChannelId.toArray) + statement.setBytes(5, ps.parts.head.toChannelId.toArray) statement.setLong(6, ps.timestamp) statement.executeUpdate() } @@ -152,8 +158,8 @@ class SqliteAuditDbSpec extends FunSuite { assert(getVersion(statement, "audit", 3) == 3) // version changed from 1 -> 3 } - // existing rows will use 00000000-0000-0000-0000-000000000000 as default - assert(migratedDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID))) + // existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default + assert(migratedDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toMillis) === Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID, parts = Seq(ps.parts.head.copy(id = ChannelCodecs.UNKNOWN_UUID))))) val postMigrationDb = new SqliteAuditDb(connection) @@ -162,12 +168,14 @@ class SqliteAuditDbSpec extends FunSuite { } postMigrationDb.add(ps1) - postMigrationDb.add(ps2) postMigrationDb.add(e1) postMigrationDb.add(e2) // the old record will have the UNKNOWN_UUID but the new ones will have their actual id - assert(postMigrationDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID), ps1, ps2)) + assert(postMigrationDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toMillis) === Seq( + ps.copy(id = ChannelCodecs.UNKNOWN_UUID, parts = Seq(ps.parts.head.copy(id = ChannelCodecs.UNKNOWN_UUID))), + ps1.copy(id = pp1.id, parts = pp1 :: Nil), + ps1.copy(id = pp2.id, parts = pp2 :: Nil))) } test("handle migration version 2 -> 3") { @@ -196,8 +204,8 @@ class SqliteAuditDbSpec extends FunSuite { assert(getVersion(statement, "audit", 3) == 2) // version 2 is deployed now } - val e1 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), true) - val e2 = ChannelErrorOccured(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), true) + val e1 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) + val e2 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true) val migratedDb = new SqliteAuditDb(connection) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index 88052c3158..993ebb4065 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -17,16 +17,15 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.TestConstants import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePendingRelayDb} import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec import fr.acinq.eclair.wire.ChannelCodecsSpec +import fr.acinq.eclair.{CltvExpiry, TestConstants} import org.scalatest.FunSuite import org.sqlite.SQLiteException import scodec.bits.ByteVector - class SqliteChannelsDbSpec extends FunSuite { test("init sqlite 2 times in a row") { @@ -44,9 +43,9 @@ class SqliteChannelsDbSpec extends FunSuite { val commitNumber = 42 val paymentHash1 = ByteVector32.Zeroes - val cltvExpiry1 = 123 + val cltvExpiry1 = CltvExpiry(123) val paymentHash2 = ByteVector32(ByteVector.fill(32)(1)) - val cltvExpiry2 = 656 + val cltvExpiry2 = CltvExpiry(656) intercept[SQLiteException](db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 925f277dd9..0529d46b23 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -16,50 +16,82 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.{Block, Crypto, Satoshi} +import java.sql.Connection + +import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.{Block, Crypto} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb -import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.db.sqlite.SqliteUtils._ +import fr.acinq.eclair.router.{Announcements, PublicChannel} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, Color, NodeAddress, Tor2} -import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite -import org.sqlite.SQLiteException +import scodec.bits.HexStringSyntax +import scala.collection.SortedMap class SqliteNetworkDbSpec extends FunSuite { val shortChannelIds = (42 to (5000 + 42)).map(i => ShortChannelId(i)) - def prune(ca: ChannelAnnouncement): ChannelAnnouncement = ca.copy( - nodeSignature1 = null, - nodeSignature2 = null, - bitcoinSignature1 = null, - bitcoinSignature2 = null, - features = null, - chainHash = null, - bitcoinKey1 = null, - bitcoinKey2 = null) - - def prune(cu: ChannelUpdate): ChannelUpdate = cu.copy(signature = null, chainHash = null) - test("init sqlite 2 times in a row") { val sqlite = TestConstants.sqliteInMemory() - val db1 = new SqliteNetworkDb(sqlite) - val db2 = new SqliteNetworkDb(sqlite) + val db1 = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) + val db2 = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) + } + + test("migration test 1->2") { + val sqlite = TestConstants.sqliteInMemory() + + using(sqlite.createStatement()) { statement => + getVersion(statement, "network", 1) // this will set version to 1 + statement.execute("PRAGMA foreign_keys = ON") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS nodes (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, txid STRING NOT NULL, data BLOB NOT NULL, capacity_sat INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_updates (short_channel_id INTEGER NOT NULL, node_flag INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(short_channel_id, node_flag), FOREIGN KEY(short_channel_id) REFERENCES channels(short_channel_id))") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_updates_idx ON channel_updates(short_channel_id)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)") + } + + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 1) + } + + // first round: this will trigger a migration + simpleTest(sqlite) + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 2) + } + + using(sqlite.createStatement()) { statement => + statement.executeUpdate("DELETE FROM nodes") + statement.executeUpdate("DELETE FROM channels") + } + + // second round: no migration + simpleTest(sqlite) + + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "network", 2) == 2) + } + } test("add/remove/list nodes") { val sqlite = TestConstants.sqliteInMemory() - val db = new SqliteNetworkDb(sqlite) + val db = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) - val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"") + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200") + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200") + val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, hex"00") 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.getNode(node_1.nodeId) === Some(node_1)) assert(db.listNodes().size === 1) db.addNode(node_2) db.addNode(node_3) @@ -72,20 +104,38 @@ class SqliteNetworkDbSpec extends FunSuite { assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) } - test("add/remove/list channels and channel_updates") { - val sqlite = TestConstants.sqliteInMemory() - val db = new SqliteNetworkDb(sqlite) + def shrink(c: ChannelAnnouncement) = c.copy(bitcoinKey1 = null, bitcoinKey2 = null, bitcoinSignature1 = null, bitcoinSignature2 = null, nodeSignature1 = null, nodeSignature2 = null, chainHash = null, features = null) + + def shrink(c: ChannelUpdate) = c.copy(signature = null) + + def simpleTest(sqlite: Connection) = { + val db = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) def sig = Crypto.sign(randomBytes32, randomKey) - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + def generatePubkeyHigherThan(priv: PrivateKey) = { + var res = priv + while (!Announcements.isNode1(priv.publicKey, res.publicKey)) res = randomKey + res + } + + // in order to differentiate channel_updates 1/2 we order public keys + val a = randomKey + 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_shrunk = shrink(channel_1) + val channel_2_shrunk = shrink(channel_2) + val channel_3_shrunk = shrink(channel_3) val txid_1 = randomBytes32 val txid_2 = randomBytes32 val txid_3 = randomBytes32 - val capacity = Satoshi(10000) + val capacity = 10000 sat assert(db.listChannels().toSet === Set.empty) db.addChannel(channel_1, txid_1, capacity) @@ -93,52 +143,65 @@ class SqliteNetworkDbSpec extends FunSuite { assert(db.listChannels().size === 1) db.addChannel(channel_2, txid_2, capacity) db.addChannel(channel_3, txid_3, capacity) - assert(db.listChannels().keySet == Set(channel_1, channel_2, channel_3).map(prune)) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, None, None), + channel_2.shortChannelId -> PublicChannel(channel_2_shrunk, txid_2, capacity, None, None), + channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, None, None))) db.removeChannel(channel_2.shortChannelId) - assert(db.listChannels().keySet == Set(channel_1, channel_3).map(prune)) - - val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(43), 5, 7000000, 50000, 100, 500000000L, true) - val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, ShortChannelId(44), 5, 7000000, 50000, 100, 500000000L, true) - - assert(db.listChannelUpdates().toSet === Set.empty) - db.addChannelUpdate(channel_update_1) - db.addChannelUpdate(channel_update_1) // duplicate is ignored - assert(db.listChannelUpdates().size === 1) - intercept[SQLiteException](db.addChannelUpdate(channel_update_2)) - db.addChannelUpdate(channel_update_3) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, None, None), + channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, None, None))) + + val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, a.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, c.publicKey, ShortChannelId(44), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + + val channel_update_1_shrunk = shrink(channel_update_1) + val channel_update_2_shrunk = shrink(channel_update_2) + val channel_update_3_shrunk = shrink(channel_update_3) + + + db.updateChannel(channel_update_1) + db.updateChannel(channel_update_1) // duplicate is ignored + db.updateChannel(channel_update_2) + db.updateChannel(channel_update_3) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, Some(channel_update_1_shrunk), Some(channel_update_2_shrunk)), + channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, Some(channel_update_3_shrunk), None))) db.removeChannel(channel_3.shortChannelId) - assert(db.listChannels().keySet === Set(channel_1).map(prune)) - assert(db.listChannelUpdates().toSet === Set(channel_update_1).map(prune)) - db.updateChannelUpdate(channel_update_1) + assert(db.listChannels() === SortedMap( + channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, Some(channel_update_1_shrunk), Some(channel_update_2_shrunk)))) + } + + test("add/remove/list channels and channel_updates") { + val sqlite = TestConstants.sqliteInMemory() + simpleTest(sqlite) } test("remove many channels") { val sqlite = TestConstants.sqliteInMemory() - val db = new SqliteNetworkDb(sqlite) + val db = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) val sig = Crypto.sign(randomBytes32, randomKey) val priv = randomKey val pub = priv.publicKey - val capacity = Satoshi(10000) + val capacity = 10000 sat val channels = shortChannelIds.map(id => Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, id, pub, pub, pub, pub, sig, sig, sig, sig)) - val template = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv, pub, ShortChannelId(42), 5, 7000000, 50000, 100, 500000000L, true) + val template = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv, pub, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) val updates = shortChannelIds.map(id => template.copy(shortChannelId = id)) val txid = randomBytes32 channels.foreach(ca => db.addChannel(ca, txid, capacity)) - updates.foreach(u => db.addChannelUpdate(u)) - assert(db.listChannels().keySet === channels.map(prune).toSet) - assert(db.listChannelUpdates() === updates.map(prune)) + updates.foreach(u => db.updateChannel(u)) + assert(db.listChannels().keySet === channels.map(_.shortChannelId).toSet) val toDelete = channels.map(_.shortChannelId).drop(500).take(2500) db.removeChannels(toDelete) - assert(db.listChannels().keySet === channels.filterNot(a => toDelete.contains(a.shortChannelId)).map(prune).toSet) - assert(db.listChannelUpdates().toSet === updates.filterNot(u => toDelete.contains(u.shortChannelId)).map(prune).toSet) + assert(db.listChannels().keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) } test("prune many channels") { val sqlite = TestConstants.sqliteInMemory() - val db = new SqliteNetworkDb(sqlite) + val db = new SqliteNetworkDb(sqlite, Block.RegtestGenesisBlock.hash) db.addToPruned(shortChannelIds) shortChannelIds.foreach { id => assert(db.isPruned((id))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index b8eec4b784..f70d18aaf0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,29 +17,32 @@ package fr.acinq.eclair.db import java.util.UUID -import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} -import fr.acinq.eclair.TestConstants.Bob -import fr.acinq.eclair.{TestConstants, payment} + +import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.db.sqlite.SqliteUtils._ +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.Hop +import fr.acinq.eclair.wire.{ChannelUpdate, UnknownNextPeer} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomBytes64, randomKey} import org.scalatest.FunSuite -import scodec.bits._ -import fr.acinq.eclair.randomBytes32 + import scala.compat.Platform -import OutgoingPaymentStatus._ -import concurrent.duration._ +import scala.concurrent.duration._ class SqlitePaymentsDbSpec extends FunSuite { + import SqlitePaymentsDbSpec._ + test("init sqlite 2 times in a row") { val sqlite = TestConstants.sqliteInMemory() val db1 = new SqlitePaymentsDb(sqlite) val db2 = new SqlitePaymentsDb(sqlite) } - test("handle version migration 1->2") { - + test("handle version migration 1->3") { val connection = TestConstants.sqliteInMemory() using(connection.createStatement()) { statement => @@ -51,137 +54,281 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 1) == 1) // version 1 is deployed now } - val oldReceivedPayment = IncomingPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 123, 1233322) - - // insert old type record + // Changes between version 1 and 2: + // - the monolithic payments table has been replaced by two tables, received_payments and sent_payments + // - old records from the payments table are ignored (not migrated to the new tables) using(connection.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => - statement.setBytes(1, oldReceivedPayment.paymentHash.toArray) - statement.setLong(2, oldReceivedPayment.amountMsat) - statement.setLong(3, oldReceivedPayment.receivedAt) + statement.setBytes(1, paymentHash1.toArray) + statement.setLong(2, (123 msat).toLong) + statement.setLong(3, 1000) // received_at statement.executeUpdate() } val preMigrationDb = new SqlitePaymentsDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "payments", 1) == 2) // version has changed from 1 to 2! + assert(getVersion(statement, "payments", 1) == 3) // version changed from 1 -> 3 } // the existing received payment can NOT be queried anymore - assert(preMigrationDb.getIncomingPayment(oldReceivedPayment.paymentHash).isEmpty) + assert(preMigrationDb.getIncomingPayment(paymentHash1).isEmpty) // add a few rows - val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) - val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") - val pr1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) + val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, 12345 msat, alice, 1000, None, OutgoingPaymentStatus.Pending) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 1) + val pr1 = IncomingPayment(i1, preimage1, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(550 msat, 1100)) - preMigrationDb.addPaymentRequest(i1, ByteVector32.Zeroes) - preMigrationDb.addIncomingPayment(pr1) preMigrationDb.addOutgoingPayment(ps1) + preMigrationDb.addIncomingPayment(i1, preimage1) + preMigrationDb.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100) - assert(preMigrationDb.listIncomingPayments() == Seq(pr1)) - assert(preMigrationDb.listOutgoingPayments() == Seq(ps1)) - assert(preMigrationDb.listPaymentRequests(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(i1)) + assert(preMigrationDb.listIncomingPayments(1, 1500) === Seq(pr1)) + assert(preMigrationDb.listOutgoingPayments(1, 1500) === Seq(ps1)) val postMigrationDb = new SqlitePaymentsDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "payments", 2) == 2) // version still to 2 + assert(getVersion(statement, "payments", 3) == 3) // version still to 3 } - assert(postMigrationDb.listIncomingPayments() == Seq(pr1)) - assert(postMigrationDb.listOutgoingPayments() == Seq(ps1)) - assert(preMigrationDb.listPaymentRequests(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(i1)) + assert(postMigrationDb.listIncomingPayments(1, 1500) === Seq(pr1)) + assert(postMigrationDb.listOutgoingPayments(1, 1500) === Seq(ps1)) } - test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { - val sqlite = TestConstants.sqliteInMemory() - val db = new SqlitePaymentsDb(sqlite) + test("handle version migration 2->3") { + val connection = TestConstants.sqliteInMemory() - // can't receive a payment without an invoice associated with it - assertThrows[IllegalArgumentException](db.addIncomingPayment(IncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), 12345678, 1513871928275L))) - - val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") - val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") - - db.addPaymentRequest(i1, ByteVector32.Zeroes) - db.addPaymentRequest(i2, ByteVector32.Zeroes) - - val p1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) - val p2 = IncomingPayment(i2.paymentHash, 12345678, 1513871928275L) - assert(db.listIncomingPayments() === Nil) - db.addIncomingPayment(p1) - db.addIncomingPayment(p2) - assert(db.listIncomingPayments().toList === List(p1, p2)) - assert(db.getIncomingPayment(p1.paymentHash) === Some(p1)) - assert(db.getIncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) - } + using(connection.createStatement()) { statement => + getVersion(statement, "payments", 2) + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)") + } - test("add/retrieve/update sent payments") { + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 2) == 2) // version 2 is deployed now + } - val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) + // Insert a bunch of old version 2 rows. + val id1 = UUID.randomUUID() + val id2 = UUID.randomUUID() + val id3 = UUID.randomUUID() + val ps1 = OutgoingPayment(id1, id1, None, randomBytes32, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000, None, OutgoingPaymentStatus.Pending) + val ps2 = OutgoingPayment(id2, id2, None, randomBytes32, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010, None, OutgoingPaymentStatus.Failed(Nil, 1050)) + val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060)) + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 1) + val pr1 = IncomingPayment(i1, preimage1, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(12345678 msat, 1090)) + val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, "Another invoice", expirySeconds = Some(30), timestamp = 1) + val pr2 = IncomingPayment(i2, preimage2, i2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + + // Changes between version 2 and 3 to sent_payments: + // - removed the status column + // - added optional payment failures + // - added optional payment success details (fees paid and route) + // - added optional payment request + // - added target node ID + // - added externalID and parentID + + using(connection.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) VALUES (?, ?, ?, ?, ?)")) { statement => + statement.setString(1, ps1.id.toString) + statement.setBytes(2, ps1.paymentHash.toArray) + statement.setLong(3, ps1.amount.toLong) + statement.setLong(4, ps1.createdAt) + statement.setString(5, "PENDING") + statement.executeUpdate() + } - val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) - val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) + using(connection.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, completed_at, status) VALUES (?, ?, ?, ?, ?, ?)")) { statement => + statement.setString(1, ps2.id.toString) + statement.setBytes(2, ps2.paymentHash.toArray) + statement.setLong(3, ps2.amount.toLong) + statement.setLong(4, ps2.createdAt) + statement.setLong(5, ps2.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt) + statement.setString(6, "FAILED") + statement.executeUpdate() + } - assert(db.listOutgoingPayments().isEmpty) - db.addOutgoingPayment(s1) - db.addOutgoingPayment(s2) + using(connection.prepareStatement("INSERT INTO sent_payments (id, payment_hash, preimage, amount_msat, created_at, completed_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setString(1, ps3.id.toString) + statement.setBytes(2, ps3.paymentHash.toArray) + statement.setBytes(3, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].paymentPreimage.toArray) + statement.setLong(4, ps3.amount.toLong) + statement.setLong(5, ps3.createdAt) + statement.setLong(6, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt) + statement.setString(7, "SUCCEEDED") + statement.executeUpdate() + } - assert(db.listOutgoingPayments().toList == Seq(s1, s2)) - assert(db.getOutgoingPayment(s1.id) === Some(s1)) - assert(db.getOutgoingPayment(s1.id).get.completedAt.isEmpty) - assert(db.getOutgoingPayment(UUID.randomUUID()) === None) - assert(db.getOutgoingPayments(s2.paymentHash) === Seq(s2)) - assert(db.getOutgoingPayments(ByteVector32.Zeroes) === Seq.empty) + // Changes between version 2 and 3 to received_payments: + // - renamed the preimage column + // - made expire_at not null + + 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.setLong(4, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) + statement.setLong(5, pr1.createdAt) + statement.setLong(6, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt) + statement.executeUpdate() + } - val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) - db.addOutgoingPayment(s3) + 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.setLong(4, pr2.createdAt) + statement.setLong(5, (i2.timestamp + i2.expiry.get).seconds.toMillis) + statement.executeUpdate() + } - db.updateOutgoingPayment(s3.id, FAILED) - assert(db.getOutgoingPayment(s3.id).get.status == FAILED) - assert(db.getOutgoingPayment(s3.id).get.preimage.isEmpty) // failed sent payments don't have a preimage - assert(db.getOutgoingPayment(s3.id).get.completedAt.isDefined) + val preMigrationDb = new SqlitePaymentsDb(connection) - // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingPayment(s3.id, SUCCEEDED)) + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 2) == 3) // version changed from 2 -> 3 + } + + assert(preMigrationDb.getIncomingPayment(i1.paymentHash) === Some(pr1)) + assert(preMigrationDb.getIncomingPayment(i2.paymentHash) === Some(pr2)) + assert(preMigrationDb.listOutgoingPayments(1, 2000) === Seq(ps1, ps2, ps3)) + + val postMigrationDb = new SqlitePaymentsDb(connection) + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 3) == 3) // version still to 3 + } - db.updateOutgoingPayment(s1.id, SUCCEEDED, Some(ByteVector32.One)) - assert(db.getOutgoingPayment(s1.id).get.preimage.isDefined) - assert(db.getOutgoingPayment(s1.id).get.completedAt.isDefined) + val i3 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, "invoice #3", expirySeconds = Some(30)) + val pr3 = IncomingPayment(i3, preimage3, i3.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) + postMigrationDb.addIncomingPayment(i3, pr3.paymentPreimage) + + val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32, 123 msat, alice, 1100, Some(i3), OutgoingPaymentStatus.Pending) + val ps5 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("2"), randomBytes32, 456 msat, bob, 1150, Some(i2), OutgoingPaymentStatus.Succeeded(preimage1, 42 msat, Nil, 1180)) + val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32, 789 msat, bob, 1250, None, OutgoingPaymentStatus.Failed(Nil, 1300)) + postMigrationDb.addOutgoingPayment(ps4) + postMigrationDb.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending)) + postMigrationDb.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32, None, 1180)))) + postMigrationDb.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) + postMigrationDb.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300)) + + assert(postMigrationDb.listOutgoingPayments(1, 2000) === Seq(ps1, ps2, ps3, ps4, ps5, ps6)) + assert(postMigrationDb.listIncomingPayments(1, Platform.currentTime) === Seq(pr1, pr2, pr3)) + assert(postMigrationDb.listExpiredIncomingPayments(1, 2000) === Seq(pr2)) } - test("add/retrieve payment requests") { + test("add/retrieve/update incoming payments") { + val sqlite = TestConstants.sqliteInMemory() + val db = new SqlitePaymentsDb(sqlite) - val someTimestamp = 12345 + // can't receive a payment without an invoice associated with it + assertThrows[IllegalArgumentException](db.receiveIncomingPayment(randomBytes32, 12345678 msat)) + + val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #1", timestamp = 1) + val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #2", timestamp = 2, expirySeconds = Some(30)) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32, expiredInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32, expiredInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired) + + val pendingInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #3") + val pendingInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #4", expirySeconds = Some(30)) + val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32, pendingInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) + val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32, pendingInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending) + + val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #5") + val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #6", expirySeconds = Some(60)) + val receivedAt1 = Platform.currentTime + 1 + val payment1 = IncomingPayment(paidInvoice1, randomBytes32, paidInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(561 msat, receivedAt1)) + val receivedAt2 = Platform.currentTime + 2 + val payment2 = IncomingPayment(paidInvoice2, randomBytes32, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) + + db.addIncomingPayment(pendingInvoice1, pendingPayment1.paymentPreimage) + db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage) + db.addIncomingPayment(expiredInvoice1, expiredPayment1.paymentPreimage) + db.addIncomingPayment(expiredInvoice2, expiredPayment2.paymentPreimage) + 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))) + + val now = Platform.currentTime + assert(db.listIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listExpiredIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2)) + assert(db.listReceivedIncomingPayments(0, now) === Nil) + assert(db.listPendingIncomingPayments(0, now) === Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + + db.receiveIncomingPayment(paidInvoice1.paymentHash, 561 msat, receivedAt1) + db.receiveIncomingPayment(paidInvoice2.paymentHash, 1111 msat, receivedAt2) + + assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1)) + assert(db.listIncomingPayments(0, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listIncomingPayments(now - 60.seconds.toMillis, now) === Seq(pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listPendingIncomingPayments(0, now) === Seq(pendingPayment1, pendingPayment2)) + assert(db.listReceivedIncomingPayments(0, now) === Seq(payment1, payment2)) + } + + test("add/retrieve/update outgoing payments") { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val bob = Bob.keyManager + val parentId = UUID.randomUUID() + val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 0) + val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, 123 msat, alice, 100, Some(i1), OutgoingPaymentStatus.Pending) + val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, 456 msat, bob, 200, None, OutgoingPaymentStatus.Pending) - val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32) + assert(db.listOutgoingPayments(0, Platform.currentTime).isEmpty) + db.addOutgoingPayment(s1) + db.addOutgoingPayment(s2) - val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = someTimestamp) - val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = Platform.currentTime.milliseconds.toSeconds) + // can't add an outgoing payment in non-pending state + assertThrows[IllegalArgumentException](db.addOutgoingPayment(s1.copy(status = OutgoingPaymentStatus.Succeeded(randomBytes32, 0 msat, Nil, 110)))) - // i2 doesn't expire - assert(i1.expiry.isEmpty && i2.expiry.isDefined) - assert(i1.amount.isDefined && i2.amount.isEmpty) + assert(db.listOutgoingPayments(1, 300).toList == Seq(s1, s2)) + assert(db.listOutgoingPayments(1, 150).toList == Seq(s1)) + assert(db.listOutgoingPayments(150, 250).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) - db.addPaymentRequest(i1, ByteVector32.Zeroes) - db.addPaymentRequest(i2, ByteVector32.One) + val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300) + val s4 = s2.copy(id = UUID.randomUUID(), createdAt = 300) + db.addOutgoingPayment(s3) + db.addOutgoingPayment(s4) - // order matters, i2 has a more recent timestamp than i1 - assert(db.listPaymentRequests(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(i2, i1)) - assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) - assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) + db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310)) + val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310)) + assert(db.getOutgoingPayment(s3.id) === Some(ss3)) + db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(new RuntimeException("woops")), RemoteFailure(Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320)) + val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", Nil), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, Some(ShortChannelId(43)))))), 320)) + assert(db.getOutgoingPayment(s4.id) === Some(ss4)) - assert(db.listPendingPaymentRequests(0, (Platform.currentTime.milliseconds + 1.minute).toSeconds) == Seq(i2, i1)) - assert(db.getPendingPaymentRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) - assert(db.getPendingPaymentRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) + // can't update again once it's in a final state + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32, None))))) + + val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, Seq( + PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32, None, 400), + PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32, Some(Seq(hop_ab, hop_bc)), 410) + )) + val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400)) + val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, Some(ShortChannelId(43)))), 410)) + 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)) - val from = (someTimestamp - 100).seconds.toSeconds - val to = (someTimestamp + 100).seconds.toSeconds - assert(db.listPaymentRequests(from, to) == Seq(i1)) + // can't update again once it's in a final state + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentFailed(s1.id, s1.paymentHash, Nil))) } } + +object SqlitePaymentsDbSpec { + 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 = Hop(alice, bob, ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(42), 1, 0, 0, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) + val hop_bc = Hop(bob, carol, ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(43), 1, 0, 0, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) + 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/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala new file mode 100644 index 0000000000..8bafb8fcb1 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -0,0 +1,77 @@ +/* + * 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.db + +import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.db.sqlite.SqliteUtils.using +import org.scalatest.FunSuite +import org.sqlite.SQLiteException + +class SqliteUtilsSpec extends FunSuite { + + test("using with auto-commit disabled") { + val conn = TestConstants.sqliteInMemory() + + using(conn.createStatement()) { statement => + statement.executeUpdate("CREATE TABLE utils_test (id INTEGER NOT NULL PRIMARY KEY, updated_at INTEGER)") + statement.executeUpdate("INSERT INTO utils_test VALUES (1, 1)") + statement.executeUpdate("INSERT INTO utils_test VALUES (2, 2)") + } + + 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.next()) + assert(results.getLong("id") === 2) + assert(!results.next()) + } + + assertThrows[SQLiteException](using(conn.createStatement(), inTransaction = true) { statement => + statement.executeUpdate("INSERT INTO utils_test VALUES (3, 3)") + statement.executeUpdate("INSERT INTO utils_test VALUES (1, 3)") // should throw (primary key violation) + }) + + 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.next()) + assert(results.getLong("id") === 2) + assert(!results.next()) + } + + using(conn.createStatement(), inTransaction = true) { statement => + statement.executeUpdate("INSERT INTO utils_test VALUES (3, 3)") + statement.executeUpdate("INSERT INTO utils_test VALUES (4, 4)") + } + + 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.next()) + assert(results.getLong("id") === 2) + assert(results.next()) + assert(results.getLong("id") === 3) + assert(results.next()) + assert(results.getLong("id") === 4) + assert(!results.next()) + } + } + +} 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 9af1ec45d1..37e618b22b 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 @@ -23,8 +23,8 @@ import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{TestKit, TestProbe} import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, MilliSatoshi, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} @@ -32,20 +32,21 @@ import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} -import fr.acinq.eclair.io.{NodeURI, Peer} +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _} -import fr.acinq.eclair.payment.{LocalPaymentHandler, PaymentRequest} +import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.ROUTE_MAX_LENGTH -import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, ChannelDesc, RouteParams} +import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, PublicChannel, RouteParams} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, Kit, Setup, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Kit, LongToBtcAmount, MilliSatoshi, Setup, ShortChannelId, TestConstants, randomBytes32} import grizzled.slf4j.Logging -import org.json4s.JsonAST.JValue -import org.json4s.{DefaultFormats, JString} +import org.json4s.JsonAST.{JString, JValue} +import org.json4s.DefaultFormats import org.scalatest.{BeforeAndAfterAll, FunSuiteLike, Ignore} import scodec.bits.ByteVector @@ -65,9 +66,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we override the default because these test were designed to use cost-optimized routes val integrationTestRouteParams = Some(RouteParams( randomize = false, - maxFeeBaseMsat = Long.MaxValue, + maxFeeBase = MilliSatoshi(Long.MaxValue), maxFeePct = Double.MaxValue, - routeMaxCltv = Int.MaxValue, + routeMaxCltv = CltvExpiryDelta(Int.MaxValue), routeMaxLength = ROUTE_MAX_LENGTH, ratios = Some(WeightRatios( cltvDeltaFactor = 0.1, @@ -79,10 +80,10 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val commonConfig = ConfigFactory.parseMap(Map( "eclair.chain" -> "regtest", "eclair.server.public-ips.1" -> "127.0.0.1", - "eclair.bitcoind.port" -> 28333, - "eclair.bitcoind.rpcport" -> 28332, - "eclair.bitcoind.zmqblock" -> "tcp://127.0.0.1:28334", - "eclair.bitcoind.zmqtx" -> "tcp://127.0.0.1:28335", + "eclair.bitcoind.port" -> bitcoindPort, + "eclair.bitcoind.rpcport" -> bitcoindRpcPort, + "eclair.bitcoind.zmqblock" -> s"tcp://127.0.0.1:$bitcoindZmqBlockPort", + "eclair.bitcoind.zmqtx" -> s"tcp://127.0.0.1:$bitcoindZmqTxPort", "eclair.mindepth-blocks" -> 2, "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, "eclair.router.broadcast-interval" -> "2 second", @@ -114,8 +115,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.receiveOne(5 second).isInstanceOf[JValue] }, max = 30 seconds, interval = 500 millis) logger.info(s"generating initial blocks...") - sender.send(bitcoincli, BitcoinReq("generate", 150)) - sender.expectMsgType[JValue](30 seconds) + generateBlocks(bitcoincli, 150, timeout = 30 seconds) } def instantiateEclairNode(name: String, config: Config) = { @@ -156,7 +156,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService nodes("C").paymentHandler ! paymentHandlerC } - def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = { + def connect(node1: Kit, node2: Kit, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi) = { val sender = TestProbe() val address = node2.nodeParams.publicAddresses.head sender.send(node1.switchboard, Peer.Connect( @@ -166,8 +166,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.expectMsgAnyOf(10 seconds, "connected", "already connected") sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, - fundingSatoshis = Satoshi(fundingSatoshis), - pushMsat = MilliSatoshi(pushMsat), + fundingSatoshis = fundingSatoshis, + pushMsat = pushMsat, fundingTxFeeratePerKw_opt = None, channelFlags = None, timeout_opt = None)) @@ -185,19 +185,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val eventListener = TestProbe() nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) - connect(nodes("A"), nodes("B"), 11000000, 0) - connect(nodes("B"), nodes("C"), 2000000, 0) - connect(nodes("C"), nodes("D"), 5000000, 0) - connect(nodes("C"), nodes("D"), 5000000, 0) - connect(nodes("B"), nodes("E"), 10000000, 0) - connect(nodes("E"), nodes("C"), 10000000, 0) - connect(nodes("C"), nodes("F1"), 5000000, 0) - connect(nodes("C"), nodes("F2"), 5000000, 0) - connect(nodes("C"), nodes("F3"), 5000000, 0) - connect(nodes("C"), nodes("F4"), 5000000, 0) - connect(nodes("C"), nodes("F5"), 5000000, 0) - connect(nodes("B"), nodes("G"), 16000000, 0) - connect(nodes("G"), nodes("C"), 16000000, 0) + connect(nodes("A"), nodes("B"), 11000000 sat, 0 msat) + connect(nodes("B"), nodes("C"), 2000000 sat, 0 msat) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("D"), 5000000 sat, 0 msat) + connect(nodes("B"), nodes("E"), 10000000 sat, 0 msat) + connect(nodes("E"), nodes("C"), 10000000 sat, 0 msat) + connect(nodes("C"), nodes("F1"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F2"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F3"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F4"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F5"), 5000000 sat, 0 msat) + connect(nodes("B"), nodes("G"), 16000000 sat, 0 msat) + connect(nodes("G"), nodes("C"), 16000000 sat, 0 msat) val numberOfChannels = 13 val channelEndpointsCount = 2 * numberOfChannels @@ -213,8 +213,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService }, max = 20 seconds, interval = 1 second) // confirming the funding tx - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2) within(60 seconds) { var count = 0 @@ -246,8 +245,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("wait for network announcements") { val sender = TestProbe() // generating more blocks so that all funding txes are buried under at least 6 blocks - sender.send(bitcoincli, BitcoinReq("generate", 4)) - sender.expectMsgType[JValue] + generateBlocks(bitcoincli, 4) // 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) @@ -257,15 +255,14 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("send an HTLC A->D") { val sender = TestProbe() - val amountMsat = MilliSatoshi(4200000) + val amountMsat = 4200000.msat // first we retrieve a payment hash from D sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment - sender.send(nodes("A").paymentInitiator, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID](5 seconds) - val ps = sender.expectMsgType[PaymentSucceeded](5 seconds) + val ps = sender.expectMsgType[PaymentSent](5 seconds) assert(ps.id == paymentId) } @@ -279,27 +276,29 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("B").register, ForwardShortId(shortIdBC, CMD_GETINFO)) val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments // 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.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimumMsat, nodes("B").nodeParams.feeBaseMsat, nodes("B").nodeParams.feeProportionalMillionth, 500000000L) + val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDeltaBlocks + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.feeBase, nodes("B").nodeParams.feeProportionalMillionth, 500000000 msat) // ...and notify B's relayer sender.send(nodes("B").relayer, LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) // we retrieve a payment hash from D - val amountMsat = MilliSatoshi(4200000) + val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, 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](5 seconds) - val ps = sender.expectMsgType[PaymentSucceeded](5 seconds) + val ps = sender.expectMsgType[PaymentSent](5 seconds) assert(ps.id == paymentId) + 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") + awaitCond({ // in the meantime, the router will have updated its state - sender.send(nodes("A").router, 'updatesMap) + sender.send(nodes("A").router, 'channelsMap) // we then put everything back like before by asking B to refresh its channel update (this will override the one we created) - val update = sender.expectMsgType[Map[ChannelDesc, ChannelUpdate]](10 seconds).apply(ChannelDesc(channelUpdateBC.shortChannelId, nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)) - update == channelUpdateBC + val u_opt = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId)) + u_opt.contains(channelUpdateBC) }, max = 30 seconds, interval = 1 seconds) // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former @@ -312,8 +311,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(channelUpdateBC_new.timestamp > channelUpdateBC.timestamp) assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks) awaitCond({ - sender.send(nodes("A").router, 'updatesMap) - val u = sender.expectMsgType[Map[ChannelDesc, ChannelUpdate]].apply(ChannelDesc(channelUpdateBC.shortChannelId, nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)) + sender.send(nodes("A").router, 'channelsMap) + val u = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]](10 seconds).apply(channelUpdateBC.shortChannelId)).get u.cltvExpiryDelta == nodes("B").nodeParams.expiryDeltaBlocks }, max = 30 seconds, interval = 1 second) } @@ -321,20 +320,20 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("send an HTLC A->D with an amount greater than capacity of B-C") { val sender = TestProbe() // first we retrieve a payment hash from D - val amountMsat = MilliSatoshi(300000000L) + val amountMsat = 300000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, 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](5 seconds) - sender.expectMsgType[PaymentSucceeded] // the payment FSM will also reply to the sender after the payment is completed + sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed } test("send an HTLC A->D with an unknown payment hash") { val sender = TestProbe() - val pr = SendPayment(100000000L, randomBytes32, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val pr = SendPaymentRequest(100000000 msat, randomBytes32, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, pr) // A will receive an error from D and won't retry @@ -343,18 +342,18 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService 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(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockCount))) } test("send an HTLC A->D with a lower amount than requested") { val sender = TestProbe() // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) + val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // A send payment of only 1 mBTC - val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(100000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -363,18 +362,18 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService 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(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockCount))) } test("send an HTLC A->D with too much overpayment") { val sender = TestProbe() // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) + val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // A send payment of 6 mBTC - val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(600000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -383,18 +382,18 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService 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(600000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockCount))) } test("send an HTLC A->D with a reasonable overpayment") { val sender = TestProbe() // first we retrieve a payment hash from D for 2 mBTC - val amountMsat = MilliSatoshi(200000000L) + val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(300000000 msat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -403,44 +402,40 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sender = TestProbe() // there are two C-D channels with 5000000 sat, so we should be able to make 7 payments worth 1000000 sat each for (_ <- 0 until 7) { - val amountMsat = MilliSatoshi(1000000000L) + val amountMsat = 1000000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 payment")) val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] - sender.expectMsgType[PaymentSucceeded] // the payment FSM will also reply to the sender after the payment is completed + sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed } } test("send an HTLC A->B->G->C using heuristics to select the route") { val sender = TestProbe() // first we retrieve a payment hash from C - val amountMsat = MilliSatoshi(2000) + val amountMsat = 2000.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amountMsat), "Change from coffee")) val pr = sender.expectMsgType[PaymentRequest](30 seconds) // 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, - SendPayment(amountMsat.amount, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) + sender.send(nodes("A").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) sender.expectMsgType[UUID](max = 60 seconds) awaitCond({ - sender.expectMsgType[PaymentResult](10 seconds) match { - case PaymentFailed(_, _, failures) => failures == Seq.empty // if something went wrong fail with a hint - case PaymentSucceeded(_, _, _, _, route) => route.exists(_.nodeId == nodes("G").nodeParams.nodeId) + sender.expectMsgType[PaymentEvent](10 seconds) match { + case PaymentFailed(_, _, failures, _) => failures == Seq.empty // if something went wrong fail with a hint + case PaymentSent(_, _, _, part :: Nil) => part.route.get.exists(_.nodeId == nodes("G").nodeParams.nodeId) + case _ => false } }, max = 30 seconds, interval = 10 seconds) } - /** - * We currently use p2pkh script Helpers.getFinalScriptPubKey - * - * @param scriptPubKey - * @return - */ + * We currently use p2pkh script Helpers.getFinalScriptPubKey + */ def scriptPubKeyToAddress(scriptPubKey: ByteVector) = Script.parse(scriptPubKey) match { case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) @@ -457,9 +452,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) + val currentBlockCount = getBlockCount + awaitCond(getBlockCount == currentBlockCount, max = 20 seconds, interval = 1 second) // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs val htlcReceiver = TestProbe() // we register this probe as the final payment handler @@ -467,7 +461,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPaymentRequest(100000000 msat, paymentHash, nodes("F1").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) paymentSender.expectMsgType[UUID](30 seconds) @@ -503,10 +497,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we then fulfill the htlc, which will make F redeem it on-chain sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we then generate one block so that the htlc success tx gets written to the blockchain - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1) // C will extract the preimage from the blockchain and fulfill the payment upstream - paymentSender.expectMsgType[PaymentSucceeded](30 seconds) + paymentSender.expectMsgType[PaymentSent](30 seconds) // at this point F should have 1 recv transactions: the redeemed htlc awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) @@ -514,8 +507,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 }, max = 30 seconds, interval = 1 second) // we then generate enough blocks so that C gets its main delayed output - sender.send(bitcoincli, BitcoinReq("generate", 145)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 145) // and C will have its main output awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) @@ -524,22 +516,27 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService (receivedByC diff previouslyReceivedByC).size == 1 }, max = 30 seconds, interval = 1 second) // we generate blocks to make tx confirm - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) awaitAnnouncements(nodes.filterKeys(_ == "A"), 9, 11, 24) } + def getBlockCount: Long = { + // we make sure that all nodes have the same value + awaitCond(nodes.values.map(_.nodeParams.currentBlockHeight).toSet.size == 1, max = 1 minute, interval = 1 second) + // and we return it (NB: it could be a different value at this point + nodes.values.head.nodeParams.currentBlockHeight + } + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") { val sender = TestProbe() // we subscribe to C's channel state transitions val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) + val currentBlockCount = getBlockCount + awaitCond(getBlockCount == currentBlockCount, max = 20 seconds, interval = 1 second) // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs val htlcReceiver = TestProbe() // we register this probe as the final payment handler @@ -547,7 +544,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPaymentRequest(100000000 msat, paymentHash, nodes("F2").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) paymentSender.expectMsgType[UUID](30 seconds) @@ -579,14 +576,14 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we then generate one block so that the htlc success tx gets written to the blockchain - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + sender.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(address) = sender.expectMsgType[JValue] + generateBlocks(bitcoincli, 1, Some(address)) // C will extract the preimage from the blockchain and fulfill the payment upstream - paymentSender.expectMsgType[PaymentSucceeded](30 seconds) + paymentSender.expectMsgType[PaymentSent](30 seconds) // at this point F should have 1 recv transactions: the redeemed htlc // we then generate enough blocks so that F gets its htlc-success delayed output - sender.send(bitcoincli, BitcoinReq("generate", 145)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 145, Some(address)) // at this point F should have 1 recv transactions: the redeemed htlc awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) @@ -601,8 +598,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService (receivedByC diff previouslyReceivedByC).size == 1 }, max = 30 seconds, interval = 1 second) // we generate blocks to make tx confirm - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2, Some(address)) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) awaitAnnouncements(nodes.filterKeys(_ == "A"), 8, 10, 22) @@ -614,9 +610,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) + val currentBlockCount = getBlockCount + awaitCond(getBlockCount == currentBlockCount, max = 20 seconds, interval = 1 second) // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs val htlcReceiver = TestProbe() // we register this probe as the final payment handler @@ -624,7 +619,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage: ByteVector = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPaymentRequest(100000000 msat, paymentHash, nodes("F3").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] @@ -638,12 +633,12 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val res = sender.expectMsgType[JValue](10 seconds) val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // we then generate enough blocks to make the htlc timeout - sender.send(bitcoincli, BitcoinReq("generate", 11)) - sender.expectMsgType[JValue](10 seconds) + sender.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(address) = sender.expectMsgType[JValue] + generateBlocks(bitcoincli, 11, Some(address)) // we generate more blocks for the htlc-timeout to reach enough confirmations awaitCond({ - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1, Some(address)) paymentSender.msgAvailable }, max = 30 seconds, interval = 1 second) // this will fail the htlc @@ -653,8 +648,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions - sender.send(bitcoincli, BitcoinReq("generate", 150)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 150, Some(address)) // at this point C should have 2 recv transactions: its main output and the htlc timeout awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) @@ -663,8 +657,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService (receivedByC diff previouslyReceivedByC).size == 2 }, max = 30 seconds, interval = 1 second) // we generate blocks to make tx confirm - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2, Some(address)) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) awaitAnnouncements(nodes.filterKeys(_ == "A"), 7, 9, 20) @@ -676,9 +669,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) + val currentBlockCount = getBlockCount + awaitCond(getBlockCount == currentBlockCount, max = 20 seconds, interval = 1 second) // NB: F has a no-op payment handler, allowing us to manually fulfill htlcs val htlcReceiver = TestProbe() // we register this probe as the final payment handler @@ -686,7 +678,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val preimage: ByteVector = randomBytes32 val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPaymentRequest(100000000 msat, paymentHash, nodes("F4").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID](30 seconds) @@ -704,12 +696,12 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_FORCECLOSE)) sender.expectMsg("ok") // we then generate enough blocks to make the htlc timeout - sender.send(bitcoincli, BitcoinReq("generate", 11)) - sender.expectMsgType[JValue](10 seconds) + sender.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(address) = sender.expectMsgType[JValue] + generateBlocks(bitcoincli, 11, Some(address)) // we generate more blocks for the claim-htlc-timeout to reach enough confirmations awaitCond({ - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1, Some(address)) paymentSender.msgAvailable }, max = 30 seconds, interval = 1 second) // this will fail the htlc @@ -719,8 +711,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions - sender.send(bitcoincli, BitcoinReq("generate", 145)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 145, Some(address)) // at this point C should have 2 recv transactions: its main output and the htlc timeout awaitCond({ sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) @@ -729,8 +720,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService (receivedByC diff previouslyReceivedByC).size == 2 }, max = 30 seconds, interval = 1 second) // we generate blocks to make tx confirm - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2, Some(address)) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) awaitAnnouncements(nodes.filterKeys(_ == "A"), 6, 8, 18) @@ -753,14 +743,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val paymentHandlerC = nodes("C").system.actorOf(LocalPaymentHandler.props(nodes("C").nodeParams)) val paymentHandlerF = nodes("F5").system.actorOf(LocalPaymentHandler.props(nodes("F5").nodeParams)) // first we make sure we are in sync with current blockchain height - sender.send(bitcoincli, BitcoinReq("getblockcount")) - val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long] - awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second) + val currentBlockCount = getBlockCount + awaitCond(getBlockCount == currentBlockCount, max = 20 seconds, interval = 1 second) // first we send 3 mBTC to F so that it has a balance - val amountMsat = MilliSatoshi(300000000L) + val amountMsat = 300000000.msat sender.send(paymentHandlerF, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(300000000L, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) + val sendReq = SendPaymentRequest(300000000 msat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) sender.send(nodes("A").paymentInitiator, sendReq) val paymentId = sender.expectMsgType[UUID] // we forward the htlc to the payment handler @@ -768,31 +757,31 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService forwardHandlerF.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] sigListener.expectMsgType[ChannelSignatureReceived] - sender.expectMsgType[PaymentSucceeded].id === paymentId + sender.expectMsgType[PaymentSent].id === paymentId // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs - def send(amountMsat: Long, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { - sender.send(paymentHandler, ReceivePayment(Some(MilliSatoshi(amountMsat)), "1 coffee")) + def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { + sender.send(paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) + val sendReq = SendPaymentRequest(amountMsat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1) sender.send(paymentInitiator, sendReq) sender.expectMsgType[UUID] } val buffer = TestProbe() - send(100000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending + send(100000000 msat, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending forwardHandlerF.expectMsgType[UpdateAddHtlc] forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(110000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending + send(110000000 msat, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending forwardHandlerF.expectMsgType[UpdateAddHtlc] forwardHandlerF.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(120000000, paymentHandlerC, nodes("F5").paymentInitiator) + send(120000000 msat, paymentHandlerC, nodes("F5").paymentInitiator) forwardHandlerC.expectMsgType[UpdateAddHtlc] forwardHandlerC.forward(buffer.ref) sigListener.expectMsgType[ChannelSignatureReceived] - send(130000000, paymentHandlerC, nodes("F5").paymentInitiator) + send(130000000 msat, paymentHandlerC, nodes("F5").paymentInitiator) forwardHandlerC.expectMsgType[UpdateAddHtlc] forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments @@ -808,19 +797,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService buffer.expectMsgType[UpdateAddHtlc] buffer.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] - val preimage1 = sender.expectMsgType[PaymentSucceeded].paymentPreimage + val preimage1 = sender.expectMsgType[PaymentSent].paymentPreimage buffer.expectMsgType[UpdateAddHtlc] buffer.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] - sender.expectMsgType[PaymentSucceeded].paymentPreimage + sender.expectMsgType[PaymentSent].paymentPreimage buffer.expectMsgType[UpdateAddHtlc] buffer.forward(paymentHandlerC) sigListener.expectMsgType[ChannelSignatureReceived] - sender.expectMsgType[PaymentSucceeded].paymentPreimage + sender.expectMsgType[PaymentSent].paymentPreimage buffer.expectMsgType[UpdateAddHtlc] buffer.forward(paymentHandlerC) sigListener.expectMsgType[ChannelSignatureReceived] - sender.expectMsgType[PaymentSucceeded].paymentPreimage + sender.expectMsgType[PaymentSent].paymentPreimage // this also allows us to get the channel id val channelId = commitmentsF.channelId // we also retrieve C's default final address @@ -837,8 +826,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService Transaction.correctlySpends(htlcSuccess, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) Transaction.correctlySpends(htlcTimeout, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // we then generate blocks to make the htlc timeout (nothing will happen in the channel because all of them have already been fulfilled) - sender.send(bitcoincli, BitcoinReq("generate", 20)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 20) // then we publish F's revoked transactions sender.send(bitcoincli, BitcoinReq("sendrawtransaction", revokedCommitTx.toString())) sender.expectMsgType[JValue](10000 seconds) @@ -854,8 +842,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService (receivedByC diff previouslyReceivedByC).size == 6 }, max = 30 seconds, interval = 1 second) // we generate blocks to make tx confirm - sender.send(bitcoincli, BitcoinReq("generate", 2)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 2) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) // this will remove the channel @@ -867,16 +854,16 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random logger.info(s"generating fake channels") val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("getnewaddress")) + val JString(address) = sender.expectMsgType[JValue] val channels = for (i <- 0 until 242) yield { // let's generate a block every 10 txs so that we can compute short ids if (i % 10 == 0) { - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1, Some(address)) } AnnouncementsBatchValidationSpec.simulateChannel } - sender.send(bitcoincli, BitcoinReq("generate", 1)) - sender.expectMsgType[JValue](10 seconds) + generateBlocks(bitcoincli, 1, Some(address)) logger.info(s"simulated ${channels.size} channels") val remoteNodeId = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey 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 6697116750..6b3468d35b 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.interop.rustytests import java.io.File +import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.{CountDownLatch, TimeUnit} import akka.actor.{ActorRef, ActorSystem, Props} @@ -28,7 +29,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.NoopPaymentHandler import fr.acinq.eclair.wire.Init -import fr.acinq.eclair.{Globals, TestUtils} +import fr.acinq.eclair.{LongToBtcAmount, TestUtils} import org.scalatest.{BeforeAndAfterAll, Matchers, Outcome, fixture} import scala.concurrent.duration._ @@ -43,7 +44,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix case class FixtureParam(ref: List[String], res: List[String]) override def withFixture(test: OneArgTest): Outcome = { - Globals.blockCount.set(0) + val blockCount = new AtomicLong(0) val latch = new CountDownLatch(1) val pipe: ActorRef = system.actorOf(Props(new SynchronizationPipe(latch))) val alice2blockchain = TestProbe() @@ -54,13 +55,13 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix val router = TestProbe() val wallet = new TestWallet val feeEstimator = new TestFeeEstimator - val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer)) - val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams.copy(onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer)) + val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer)) + val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams.copy(blockCount = blockCount, onChainFeeConf = Bob.nodeParams.onChainFeeConf.copy(feeEstimator = feeEstimator)), wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer)) val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures) val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(10000)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000, 1000000000, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, ChannelVersion.STANDARD) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit) pipe ! (alice, bob) within(30 seconds) { @@ -90,7 +91,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix 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("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) } 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 2c72623576..abfb034db9 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 @@ -22,13 +22,13 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.{TestConstants, TestUtils} import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, TestConstants, TestUtils} /** - * Created by PM on 30/05/2016. - */ + * Created by PM on 30/05/2016. + */ /* @@ -57,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging script match { case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) + resolve(x) ! CMD_ADD_HTLC(MilliSatoshi(amount.toInt), ByteVector32.fromValidHex(rhash), CltvExpiry(144), TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) exec(rest, a, b) case fulfill(x, id, r) :: rest => resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) @@ -134,15 +134,15 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging s" Commit ${d.commitments.localCommit.index}:", s" Offered htlcs: ${localCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", s" Received htlcs: ${localCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Balance us: ${localCommit.spec.toLocalMsat}", - s" Balance them: ${localCommit.spec.toRemoteMsat}", + s" Balance us: ${localCommit.spec.toLocal}", + s" Balance them: ${localCommit.spec.toRemote}", s" Fee rate: ${localCommit.spec.feeratePerKw}", "REMOTE COMMITS:", s" Commit ${remoteCommit.index}:", s" Offered htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", s" Received htlcs: ${remoteCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.add.id, h.add.amountMsat)).mkString(" ")}", - s" Balance us: ${remoteCommit.spec.toLocalMsat}", - s" Balance them: ${remoteCommit.spec.toRemoteMsat}", + s" Balance us: ${remoteCommit.spec.toLocal}", + s" Balance them: ${remoteCommit.spec.toRemote}", s" Fee rate: ${remoteCommit.spec.feeratePerKw}") .foreach(s => { fout.write(rtrim(s)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala index 4378f5cf1d..3094c7db5d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala @@ -19,15 +19,15 @@ package fr.acinq.eclair.io import akka.actor.{ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{ChannelCodecsSpec, TemporaryNodeFailure, UpdateAddHtlc} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, randomBytes32} import org.scalatest.FunSuiteLike import scala.concurrent.duration._ /** - * Created by PM on 27/01/2017. - */ + * Created by PM on 27/01/2017. + */ class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { @@ -36,11 +36,11 @@ class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val data = ChannelCodecsSpec.normal // assuming that data has incoming htlcs 0 and 1, we don't care about the amount/payment_hash/onion fields - val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, TestConstants.emptyOnionPacket) - val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add0 = UpdateAddHtlc(data.channelId, 0, 20000 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(data.channelId, 1, 30000 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket) // unrelated htlc - val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678 msat, randomBytes32, CltvExpiry(100), TestConstants.emptyOnionPacket) val brokenHtlcs = Seq(add0, add1, add99) val brokenHtlcKiller = system.actorOf(Props[HtlcReaper], name = "htlc-reaper") 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 da16489776..d0cf510f8e 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 @@ -22,7 +22,7 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.{ActorRef, PoisonPill} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} @@ -31,10 +31,10 @@ import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} -import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong} +import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec, SendChannelQuery} +import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream} import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, _} import scala.concurrent.duration._ @@ -43,24 +43,15 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { def ipv4FromInet4(address: InetSocketAddress) = IPv4.apply(address.getAddress.asInstanceOf[Inet4Address], address.getPort) val fakeIPAddress = NodeAddress.fromParts("1.2.3.4", 42000).get - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(100) + val shortChannelIds = RoutingSyncSpec.shortChannelIds.take(100) val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) - val channels = fakeRoutingInfo.map(_._1).toList - val updates = (fakeRoutingInfo.map(_._2) ++ fakeRoutingInfo.map(_._3)).toList - val nodes = (fakeRoutingInfo.map(_._4) ++ fakeRoutingInfo.map(_._5)).toList + val channels = fakeRoutingInfo.map(_._1.ann).toList + val updates = (fakeRoutingInfo.flatMap(_._1.update_1_opt) ++ fakeRoutingInfo.flatMap(_._1.update_2_opt)).toList + val nodes = (fakeRoutingInfo.map(_._1.ann.nodeId1) ++ fakeRoutingInfo.map(_._1.ann.nodeId2)).map(RoutingSyncSpec.makeFakeNodeAnnouncement).toList case class FixtureParam(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: TestFSMRef[Peer.State, Peer.Data, Peer]) override protected def withFixture(test: OneArgTest): Outcome = { - val aParams = Alice.nodeParams - val aliceParams = test.tags.contains("with_node_announcements") match { - case true => - val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) - aParams.db.network.addNode(bobAnnouncement) - aParams - case false => aParams - } - val authenticator = TestProbe() val watcher = TestProbe() val router = TestProbe() @@ -69,20 +60,35 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val transport = TestProbe() val wallet: EclairWallet = new TestWallet() val remoteNodeId = Bob.nodeParams.nodeId + + import com.softwaremill.quicklens._ + val aliceParams = TestConstants.Alice.nodeParams + .modify(_.syncWhitelist).setToIf(test.tags.contains("sync-whitelist-bob"))(Set(remoteNodeId)) + .modify(_.syncWhitelist).setToIf(test.tags.contains("sync-whitelist-random"))(Set(randomKey.publicKey)) + + if (test.tags.contains("with_node_announcements")) { + val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + aliceParams.db.network.addNode(bobAnnouncement) + } + val peer: TestFSMRef[Peer.State, Peer.Data, Peer] = TestFSMRef(new Peer(aliceParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } - def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty): Unit = { + def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty, remoteInit: wire.Init = wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures), expectSync: Boolean = false): Unit = { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, channels)) authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] - transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) + transport.send(peer, remoteInit) transport.expectMsgType[TransportHandler.ReadAck] - router.expectNoMsg(1 second) // bob's features require no sync + if (expectSync) { + router.expectMsgType[SendChannelQuery] + } else { + router.expectNoMsg(1 second) + } probe.send(peer, Peer.GetPeerInfo) assert(probe.expectMsgType[Peer.PeerInfo].state == "CONNECTED") } @@ -248,7 +254,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, Satoshi(12300), MilliSatoshi(0), None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 12300 sat, 0 msat, None, None, None)) awaitCond(peer.stateData.channels.nonEmpty) val channelCreated = probe.expectMsgType[ChannelCreated] @@ -256,6 +262,26 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(channelCreated.fundingTxFeeratePerKw.get == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.fundingBlockTarget)) } + // ignored on Android + ignore("sync if no whitelist is defined") { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) + } + + // ignored on Android + ignore("sync if whitelist contains peer", Tag("sync-whitelist-bob")) { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) + } + + test("don't sync if whitelist doesn't contain peer", Tag("sync-whitelist-random")) { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = false) + } + test("reply to ping") { f => import f._ val probe = TestProbe() @@ -337,7 +363,10 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val probe = TestProbe() connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer) - val query = wire.QueryShortChannelIds(Alice.nodeParams.chainHash, ChannelRangeQueries.encodeShortChannelIdsSingle(Seq(ShortChannelId(42000)), ChannelRangeQueries.UNCOMPRESSED_FORMAT, useGzip = false)) + val query = QueryShortChannelIds( + Alice.nodeParams.chainHash, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), + TlvStream.empty) // make sure that routing messages go through for (ann <- channels ++ updates) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index ccc65efe0b..e2bbb33201 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -16,85 +16,83 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC} +import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayFailure, RelayPayload, RelaySuccess} import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.Onion.RelayLegacyPayload import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} -import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite import scala.collection.mutable class ChannelSelectionSpec extends FunSuite { + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + /** - * This is just a simplified helper function with random values for fields we are not using here - */ - def dummyUpdate(shortChannelId: ShortChannelId, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, htlcMaximumMsat: Long, enable: Boolean = true) = + * This is just a simplified helper function with random values for fields we are not using here + */ + def dummyUpdate(shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true) = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, shortChannelId, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsat, enable) test("convert to CMD_FAIL_HTLC/CMD_ADD_HTLC") { + val onionPayload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), - payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), + add = UpdateAddHtlc(randomBytes32, 42, 1000000 msat, randomBytes32, CltvExpiry(70), TestConstants.emptyOnionPacket), + payload = onionPayload, nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) - val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) - - implicit val log = akka.event.NoLogging + val channelUpdate = dummyUpdate(ShortChannelId(12345), CltvExpiryDelta(10), 100 msat, 1000 msat, 100, 10000000 msat, true) // nominal case - assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket, upstream = Right(relayPayload.add), commit = true))) + assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amountToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltv, relayPayload.nextPacket, upstream = Right(relayPayload.add), commit = true))) // no channel_update assert(Relayer.relayOrFail(relayPayload, channelUpdate_opt = None) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled - val channelUpdate_disabled = channelUpdate.copy(channelFlags = Announcements.makeChannelFlags(true, enable = false)) + val channelUpdate_disabled = channelUpdate.copy(channelFlags = Announcements.makeChannelFlags(isNode1 = true, enable = false)) assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate_disabled)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(ChannelDisabled(channelUpdate_disabled.messageFlags, channelUpdate_disabled.channelFlags, channelUpdate_disabled)), commit = true))) // amount too low - val relayPayload_toolow = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 99)) - assert(Relayer.relayOrFail(relayPayload_toolow, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(AmountBelowMinimum(relayPayload_toolow.payload.amtToForward, channelUpdate)), commit = true))) + val relayPayload_toolow = relayPayload.copy(payload = onionPayload.copy(amountToForward = 99 msat)) + assert(Relayer.relayOrFail(relayPayload_toolow, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(AmountBelowMinimum(relayPayload_toolow.payload.amountToForward, channelUpdate)), commit = true))) // incorrect cltv expiry - val relayPayload_incorrectcltv = relayPayload.copy(payload = relayPayload.payload.copy(outgoingCltvValue = 42)) - assert(Relayer.relayOrFail(relayPayload_incorrectcltv, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(IncorrectCltvExpiry(relayPayload_incorrectcltv.payload.outgoingCltvValue, channelUpdate)), commit = true))) + val relayPayload_incorrectcltv = relayPayload.copy(payload = onionPayload.copy(outgoingCltv = CltvExpiry(42))) + assert(Relayer.relayOrFail(relayPayload_incorrectcltv, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(IncorrectCltvExpiry(relayPayload_incorrectcltv.payload.outgoingCltv, channelUpdate)), commit = true))) // insufficient fee - val relayPayload_insufficientfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 998910)) + val relayPayload_insufficientfee = relayPayload.copy(payload = onionPayload.copy(amountToForward = 998910 msat)) assert(Relayer.relayOrFail(relayPayload_insufficientfee, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! - val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket, upstream = Right(relayPayload.add), commit = true))) + val relayPayload_highfee = relayPayload.copy(payload = onionPayload.copy(amountToForward = 900000 msat)) + assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amountToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltv, relayPayload_highfee.nextPacket, upstream = Right(relayPayload.add), commit = true))) } test("channel selection") { - + val onionPayload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), - payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), + add = UpdateAddHtlc(randomBytes32, 42, 1000000 msat, randomBytes32, CltvExpiry(70), TestConstants.emptyOnionPacket), + payload = onionPayload, nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) - val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) + val channelUpdate = dummyUpdate(ShortChannelId(12345), CltvExpiryDelta(10), 100 msat, 1000 msat, 100, 10000000 msat, true) val channelUpdates = Map( - ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000000)), - ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 20000000)), - ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 10000000)), - ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000)), - ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, makeCommitments(ByteVector32.Zeroes, 1000000)) + ShortChannelId(11111) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000000 msat)), + ShortChannelId(12345) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 20000000 msat)), + ShortChannelId(22222) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 10000000 msat)), + ShortChannelId(33333) -> OutgoingChannel(a, channelUpdate, makeCommitments(ByteVector32.Zeroes, 100000 msat)), + ShortChannelId(44444) -> OutgoingChannel(b, channelUpdate, makeCommitments(ByteVector32.Zeroes, 1000000 msat)) ) val node2channels = new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId] node2channels.put(a, mutable.Set(ShortChannelId(12345), ShortChannelId(11111), ShortChannelId(22222), ShortChannelId(33333))) node2channels.put(b, mutable.Set(ShortChannelId(44444))) - implicit val log = akka.event.NoLogging - - import com.softwaremill.quicklens._ - // select the channel to the same node, with the lowest balance but still high enough to handle the payment assert(Relayer.selectPreferredChannel(relayPayload, channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(22222))) // select 2nd-to-best channel @@ -104,14 +102,13 @@ class ChannelSelectionSpec extends FunSuite { // all the suitable channels have been tried assert(Relayer.selectPreferredChannel(relayPayload, channelUpdates, node2channels, Seq(ShortChannelId(22222), ShortChannelId(12345), ShortChannelId(11111))) === None) // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.add.amountMsat).setTo(60000000).modify(_.payload.amtToForward).setTo(50000000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(11111))) + assert(Relayer.selectPreferredChannel(relayPayload.copy(add = relayPayload.add.copy(amountMsat = 60000000 msat), payload = onionPayload.copy(amountToForward = 50000000 msat)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(11111))) // lower amount payment - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(1000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(33333))) + assert(Relayer.selectPreferredChannel(relayPayload.copy(payload = onionPayload.copy(amountToForward = 1000 msat)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(33333))) // payment too high, no suitable channel found - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.amtToForward).setTo(1000000000), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) + assert(Relayer.selectPreferredChannel(relayPayload.copy(payload = onionPayload.copy(amountToForward = 1000000000 msat)), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) // invalid cltv expiry, no suitable channel, we keep the requested one - assert(Relayer.selectPreferredChannel(relayPayload.modify(_.payload.outgoingCltvValue).setTo(40), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) - + assert(Relayer.selectPreferredChannel(relayPayload.copy(payload = onionPayload.copy(outgoingCltv = CltvExpiry(40))), channelUpdates, node2channels, Seq.empty) === Some(ShortChannelId(12345))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 9114a8e418..592bbfd685 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -25,21 +25,23 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.{DecryptedPacket, PacketAndSecrets} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop -import fr.acinq.eclair.wire.{ChannelUpdate, OnionCodecs, PerHopPayload} -import fr.acinq.eclair.{ShortChannelId, TestConstants, nodeFee, randomBytes32} -import org.scalatest.FunSuite +import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, PerHopPayload, RelayLegacyPayload} +import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv} +import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, nodeFee, randomBytes32} +import org.scalatest.{BeforeAndAfterAll, FunSuite} import scodec.bits.ByteVector /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ -class HtlcGenerationSpec extends FunSuite { +class HtlcGenerationSpec extends FunSuite with BeforeAndAfterAll { test("compute fees") { - val feeBaseMsat = 150000L + val feeBaseMsat = 150000 msat val feeProportionalMillionth = 4L - val htlcAmountMsat = 42000000 + val htlcAmountMsat = 42000000 msat // spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000 val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000 val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat) @@ -49,110 +51,101 @@ class HtlcGenerationSpec extends FunSuite { import HtlcGenerationSpec._ test("compute payloads with fees and expiry delta") { - - val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) + val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(hops.drop(1), FinalLegacyPayload(finalAmountMsat, finalExpiry)) + val expectedPayloads = Seq[PerHopPayload]( + RelayLegacyPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc), + RelayLegacyPayload(channelUpdate_cd.shortChannelId, amount_cd, expiry_cd), + RelayLegacyPayload(channelUpdate_de.shortChannelId, amount_de, expiry_de), + FinalLegacyPayload(finalAmountMsat, finalExpiry)) assert(firstAmountMsat === amount_ab) assert(firstExpiry === expiry_ab) - assert(payloads === - PerHopPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc) :: - PerHopPayload(channelUpdate_cd.shortChannelId, amount_cd, expiry_cd) :: - PerHopPayload(channelUpdate_de.shortChannelId, amount_de, expiry_de) :: - PerHopPayload(ShortChannelId(0L), finalAmountMsat, finalExpiry) :: Nil) + assert(payloads === expectedPayloads) } - test("build onion") { - - val (_, _, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) + def testBuildOnion(legacy: Boolean): Unit = { + val finalPayload = if (legacy) { + FinalLegacyPayload(finalAmountMsat, finalExpiry) + } else { + FinalTlvPayload(TlvStream[OnionTlv](AmountToForward(finalAmountMsat), OutgoingCltv(finalExpiry))) + } + val (_, _, payloads) = buildPayloads(hops.drop(1), finalPayload) val nodes = hops.map(_.nextNodeId) val PacketAndSecrets(packet_b, _) = buildOnion(nodes, payloads, paymentHash) assert(packet_b.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion + testPeelOnion(packet_b) + } + + def testPeelOnion(packet_b: OnionRoutingPacket): Unit = { val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, packet_b) - val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + val payload_b = OnionCodecs.relayPerHopPayloadCodec.decode(bin_b.toBitVector).require.value assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_b.amtToForward === amount_bc) - assert(payload_b.outgoingCltvValue === expiry_bc) + assert(payload_b.amountToForward === amount_bc) + assert(payload_b.outgoingCltv === expiry_bc) val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) - val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value + val payload_c = OnionCodecs.relayPerHopPayloadCodec.decode(bin_c.toBitVector).require.value assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_c.amtToForward === amount_cd) - assert(payload_c.outgoingCltvValue === expiry_cd) + assert(payload_c.amountToForward === amount_cd) + assert(payload_c.outgoingCltv === expiry_cd) val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) - val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value + val payload_d = OnionCodecs.relayPerHopPayloadCodec.decode(bin_d.toBitVector).require.value assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_d.amtToForward === amount_de) - assert(payload_d.outgoingCltvValue === expiry_de) + assert(payload_d.amountToForward === amount_de) + assert(payload_d.outgoingCltv === expiry_de) val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) - val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value + val payload_e = OnionCodecs.finalPerHopPayloadCodec.decode(bin_e.toBitVector).require.value assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_e.amtToForward === finalAmountMsat) - assert(payload_e.outgoingCltvValue === finalExpiry) + assert(payload_e.amount === finalAmountMsat) + assert(payload_e.expiry === finalExpiry) } - test("build a command including the onion") { + test("build onion with final legacy payload") { + testBuildOnion(legacy = true) + } - val (add, _) = buildCommand(UUID.randomUUID, finalAmountMsat, finalExpiry, paymentHash, hops) + test("build onion with final tlv payload") { + testBuildOnion(legacy = false) + } - assert(add.amountMsat > finalAmountMsat) + test("build a command including the onion") { + val (add, _) = buildCommand(UUID.randomUUID, paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) + assert(add.amount > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) - val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_b.amtToForward === amount_bc) - assert(payload_b.outgoingCltvValue === expiry_bc) - - val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) - val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_c.amtToForward === amount_cd) - assert(payload_c.outgoingCltvValue === expiry_cd) - - val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) - val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_d.amtToForward === amount_de) - assert(payload_d.outgoingCltvValue === expiry_de) - - val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) - val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_e.amtToForward === finalAmountMsat) - assert(payload_e.outgoingCltvValue === finalExpiry) + testPeelOnion(add.onion) } test("build a command with no hops") { - val (add, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) - - assert(add.amountMsat === finalAmountMsat) + val (add, _) = buildCommand(UUID.randomUUID(), paymentHash, hops.take(1), FinalLegacyPayload(finalAmountMsat, finalExpiry)) + assert(add.amount === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion val Right(DecryptedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) - val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + val payload_b = OnionCodecs.relayPerHopPayloadCodec.decode(bin_b.toBitVector).require.value assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) - assert(payload_b.amtToForward === finalAmountMsat) - assert(payload_b.outgoingCltvValue === finalExpiry) + assert(payload_b.amountToForward === finalAmountMsat) + assert(payload_b.outgoingCltv === finalExpiry) } } object HtlcGenerationSpec { - def makeCommitments(channelId: ByteVector32, availableBalanceForSend: Long = 50000000L, availableBalanceForReceive: Long = 50000000L) = + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat) = new Commitments(ChannelVersion.STANDARD, null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) { - override lazy val availableBalanceForSendMsat: Long = availableBalanceForSend.max(0) - override lazy val availableBalanceForReceiveMsat: Long = availableBalanceForReceive.max(0) + override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) + override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } def randomExtendedPrivateKey: ExtendedPrivateKey = DeterministicWallet.generate(randomBytes32) @@ -160,11 +153,11 @@ object HtlcGenerationSpec { val (priv_a, priv_b, priv_c, priv_d, priv_e) = (TestConstants.Alice.keyManager.nodeKey, TestConstants.Bob.keyManager.nodeKey, randomExtendedPrivateKey, randomExtendedPrivateKey, randomExtendedPrivateKey) val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey) val sig = Crypto.sign(Crypto.sha256(ByteVector.empty), priv_a.privateKey) - val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, 0, 0, 42000, 0, 0, Some(500000000L)) - val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(1), cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7) - val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(2), cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4) - val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(3), cltvExpiryDelta = 10, feeBaseMsat = 60000, feeProportionalMillionths = 1) - val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(4), cltvExpiryDelta = 7, feeBaseMsat = 766000, feeProportionalMillionths = 10) + val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, 0, CltvExpiryDelta(0), 42000 msat, 0 msat, 0, Some(500000000 msat)) + val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(1), cltvExpiryDelta = CltvExpiryDelta(4), feeBaseMsat = 642000 msat, feeProportionalMillionths = 7) + val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(2), cltvExpiryDelta = CltvExpiryDelta(5), feeBaseMsat = 153000 msat, feeProportionalMillionths = 4) + val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(3), cltvExpiryDelta = CltvExpiryDelta(10), feeBaseMsat = 60000 msat, feeProportionalMillionths = 1) + val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = ShortChannelId(4), cltvExpiryDelta = CltvExpiryDelta(7), feeBaseMsat = 766000 msat, feeProportionalMillionths = 10) // simple route a -> b -> c -> d -> e @@ -174,13 +167,13 @@ object HtlcGenerationSpec { Hop(c, d, channelUpdate_cd) :: Hop(d, e, channelUpdate_de) :: Nil - val finalAmountMsat = 42000000L - val currentBlockCount = 420000 - val finalExpiry = currentBlockCount + Channel.MIN_CLTV_EXPIRY + val finalAmountMsat = 42000000 msat + val currentBlockCount = 400000 + val finalExpiry = CltvExpiry(currentBlockCount) + Channel.MIN_CLTV_EXPIRY_DELTA val paymentPreimage = randomBytes32 val paymentHash = Crypto.sha256(paymentPreimage) - val expiry_de = currentBlockCount + Channel.MIN_CLTV_EXPIRY + val expiry_de = finalExpiry val amount_de = finalAmountMsat val fee_d = nodeFee(channelUpdate_de.feeBaseMsat, channelUpdate_de.feeProportionalMillionths, amount_de) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 3915d24895..a66733a12b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -16,24 +16,26 @@ package fr.acinq.eclair.payment -import akka.actor.Status.Failure import akka.actor.ActorSystem +import akka.actor.Status.Failure import akka.testkit.{TestActorRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} +import fr.acinq.eclair.db.IncomingPaymentStatus import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment +import fr.acinq.eclair.payment.PaymentReceived.PartialPayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} -import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomKey} +import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector import scala.concurrent.duration._ /** - * Created by PM on 24/03/2017. - */ + * Created by PM on 24/03/2017. + */ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { @@ -44,48 +46,54 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived]) - val amountMsat = MilliSatoshi(42000) - val expiry = Globals.blockCount.get() + 12 + val amountMsat = 42000 msat + val expiry = CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight) { sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - assert(nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).isDefined) - assert(!nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).get._2.isExpired) + val incoming = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(incoming.isDefined) + assert(incoming.get.status === IncomingPaymentStatus.Pending) + assert(!incoming.get.paymentRequest.isExpired) + assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] - assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(paymentRelayed.copy(parts = paymentRelayed.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil)) + val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0) === IncomingPaymentStatus.Received(amountMsat, 0)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] - assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(paymentRelayed.copy(parts = paymentRelayed.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil)) + val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0) === IncomingPaymentStatus.Received(amountMsat, 0)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, cltvExpiry = CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) sender.send(handler, add) - assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) + assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) eventListener.expectNoMsg(300 milliseconds) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) } } @@ -97,19 +105,19 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived]) // negative amount should fail - sender.send(handler, ReceivePayment(Some(MilliSatoshi(-50)), "1 coffee")) + sender.send(handler, ReceivePayment(Some(-50 msat), "1 coffee")) val negativeError = sender.expectMsgType[Failure] assert(negativeError.cause.getMessage.contains("amount is not valid")) // amount = 0 should fail - sender.send(handler, ReceivePayment(Some(MilliSatoshi(0)), "1 coffee")) + sender.send(handler, ReceivePayment(Some(0 msat), "1 coffee")) val zeroError = sender.expectMsgType[Failure] assert(zeroError.cause.getMessage.contains("amount is not valid")) // success with 1 mBTC - sender.send(handler, ReceivePayment(Some(MilliSatoshi(100000000L)), "1 coffee")) + sender.send(handler, ReceivePayment(Some(100000000 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.contains(MilliSatoshi(100000000L)) && pr.nodeId.toString == nodeParams.nodeId.toString) + assert(pr.amount.contains(100000000 msat) && pr.nodeId.toString == nodeParams.nodeId.toString) } test("Payment request generation should succeed when the amount is not set") { @@ -125,10 +133,10 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams)) val sender = TestProbe() - sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee")) + sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee")) assert(sender.expectMsgType[PaymentRequest].expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) - sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with custom expiry", expirySeconds_opt = Some(60))) + sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee with custom expiry", expirySeconds_opt = Some(60))) assert(sender.expectMsgType[PaymentRequest].expiry === Some(60)) } @@ -138,16 +146,16 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val x = randomKey.publicKey val y = randomKey.publicKey - val extraHop_x_y = ExtraHop(x, ShortChannelId(1), 10, 11, 12) - val extraHop_y_z = ExtraHop(y, ShortChannelId(2), 20, 21, 22) - val extraHop_x_t = ExtraHop(x, ShortChannelId(3), 30, 31, 32) + val extraHop_x_y = ExtraHop(x, ShortChannelId(1), 10 msat, 11, CltvExpiryDelta(12)) + val extraHop_y_z = ExtraHop(y, ShortChannelId(2), 20 msat, 21, CltvExpiryDelta(22)) + val extraHop_x_t = ExtraHop(x, ShortChannelId(3), 30 msat, 31, CltvExpiryDelta(32)) val route_x_z = extraHop_x_y :: extraHop_y_z :: Nil val route_x_t = extraHop_x_t :: Nil - sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with additional routing info", extraHops = List(route_x_z, route_x_t))) + sender.send(handler, ReceivePayment(Some(42000 msat), "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)) - sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee without routing info")) + sender.send(handler, ReceivePayment(Some(42000 msat), "1 coffee without routing info")) assert(sender.expectMsgType[PaymentRequest].routingInfo === Nil) } @@ -158,16 +166,17 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived]) - val amountMsat = MilliSatoshi(42000) - val expiry = Globals.blockCount.get() + 12 + val amountMsat = 42000 msat + val expiry = CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight) sender.send(handler, ReceivePayment(Some(amountMsat), "some desc", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FAIL_HTLC] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) + val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) } } 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 new file mode 100644 index 0000000000..a059dcc19c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -0,0 +1,61 @@ +/* + * 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 java.util.UUID + +import akka.actor.ActorSystem +import akka.testkit.{TestKit, TestProbe} +import fr.acinq.eclair.payment.HtlcGenerationSpec._ +import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.router.{FinalizeRoute, RouteParams, RouteRequest} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants} +import org.scalatest.FunSuiteLike + +/** + * Created by t-bast on 25/07/2019. + */ + +class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + + test("forward payment with pre-defined route") { + val sender = TestProbe() + val router = TestProbe() + val paymentInitiator = system.actorOf(PaymentInitiator.props(TestConstants.Alice.nodeParams, router.ref, TestProbe().ref)) + + sender.send(paymentInitiator, PaymentInitiator.SendPaymentRequest(finalAmountMsat, paymentHash, c, 1, predefinedRoute = Seq(a, b, c))) + sender.expectMsgType[UUID] + router.expectMsg(FinalizeRoute(Seq(a, b, c))) + } + + test("forward legacy payment") { + val sender = TestProbe() + val router = TestProbe() + val paymentInitiator = system.actorOf(PaymentInitiator.props(TestConstants.Alice.nodeParams, router.ref, TestProbe().ref)) + + val hints = Seq(Seq(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) + val routeParams = RouteParams(randomize = true, 15 msat, 1.5, 5, CltvExpiryDelta(561), None) + sender.send(paymentInitiator, PaymentInitiator.SendPaymentRequest(finalAmountMsat, paymentHash, c, 1, CltvExpiryDelta(42), assistedRoutes = hints, routeParams = Some(routeParams))) + sender.expectMsgType[UUID] + router.expectMsg(RouteRequest(TestConstants.Alice.nodeParams.nodeId, c, finalAmountMsat, assistedRoutes = hints, routeParams = Some(routeParams))) + + sender.send(paymentInitiator, PaymentInitiator.SendPaymentRequest(finalAmountMsat, paymentHash, e, 3)) + sender.expectMsgType[UUID] + router.expectMsg(RouteRequest(TestConstants.Alice.nodeParams.nodeId, e, finalAmountMsat)) + } + +} 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 1bbf4e30b1..01f1c8e352 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 @@ -22,35 +22,43 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Transaction, TxOut} +import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchEventSpentBasic, WatchSpentBasic} import fr.acinq.eclair.channel.Register.ForwardShortId -import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} +import fr.acinq.eclair.channel.{AddHtlcFailed, Channel, ChannelUnavailable} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.db.OutgoingPaymentStatus +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle._ +import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ -import fr.acinq.eclair._ +import scodec.bits.HexStringSyntax /** - * Created by PM on 29/08/2016. - */ + * Created by PM on 29/08/2016. + */ class PaymentLifecycleSpec extends BaseRouterSpec { - val defaultAmountMsat = 142000000L + val defaultAmountMsat = 142000000 msat + val defaultExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA + val defaultPaymentHash = randomBytes32 + val defaultExternalId = UUID.randomUUID().toString + val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId)) test("send to route") { fixture => import fixture._ - val defaultPaymentHash = randomBytes32 val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) - val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) + val paymentDb = nodeParams.db.payments + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -60,41 +68,60 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) // pre-computed route going from A to D - val request = SendPaymentToRoute(defaultAmountMsat, defaultPaymentHash, Seq(a,b,c,d)) + val request = SendPaymentToRoute(defaultPaymentHash, Seq(a, b, c, d), FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) + val Some(outgoing) = paymentDb.getOutgoingPayment(id) + assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, id, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) - sender.expectMsgType[PaymentSucceeded] - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + sender.expectMsgType[PaymentSent] + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) + } + + test("send to route (edges not found in the graph)") { fixture => + import fixture._ + + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) + val id = UUID.randomUUID() + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, nodeParams.db.payments) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, router, TestProbe().ref)) + val sender = TestProbe() + val eventListener = TestProbe() + system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) + + val brokenRoute = SendPaymentToRoute(randomBytes32, Seq(randomKey.publicKey, randomKey.publicKey, randomKey.publicKey), FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight))) + sender.send(paymentFSM, brokenRoute) + val failureMessage = eventListener.expectMsgType[PaymentFailed].failures.head.asInstanceOf[LocalFailure].t.getMessage + assert(failureMessage == "Not all the nodes in the supplied route are connected with public channels") } test("payment failed (route not found)") { fixture => import fixture._ - val defaultPaymentHash = randomBytes32 val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest.copy(targetNodeId = f), paymentDb) val routerForwarder = TestProbe() - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, routerForwarder.ref, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, routerForwarder.ref, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f, maxAttempts = 5) + val request = SendPayment(defaultPaymentHash, f, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 5) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) routerForwarder.forward(router, routeRequest) - sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + assert(sender.expectMsgType[PaymentFailed].failures === LocalFailure(RouteNotFound) :: Nil) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } test("payment failed (route too expensive)") { fixture => @@ -102,39 +129,40 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, routeParams = Some(RouteParams(randomize = false, maxFeeBaseMsat = 100, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None)), maxAttempts = 5) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 5, routeParams = Some(RouteParams(randomize = false, maxFeeBase = 100 msat, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = CltvExpiryDelta(2016), ratios = None))) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } test("payment failed (unparsable failure)") { fixture => import fixture._ - val defaultPaymentHash = randomBytes32 val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -157,8 +185,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed + assert(sender.expectMsgType[PaymentFailed].failures === UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => @@ -168,62 +196,63 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, maxAttempts = 2) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, request.paymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None))) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => import fixture._ - val defaultPaymentHash = randomBytes32 val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION)) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -232,14 +261,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, nodeParams.db.payments) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, maxAttempts = 2) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData @@ -262,7 +292,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil) } test("payment failed (Update)") { fixture => @@ -272,16 +302,17 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, maxAttempts = 5) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 5) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -291,14 +322,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) - val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_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, 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 && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -308,8 +339,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { relayer.expectMsg(ForwardShortId(channelId_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 = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) - val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) + val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(43), htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_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, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2))) @@ -323,27 +354,29 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(hops2, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(hops2, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } - test("payment failed (PermanentChannelFailure)") { fixture => + def testPermanentFailure(fixture: FixtureParam, failure: FailureMessage): Unit = { import fixture._ val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() + routerForwarder.ignoreMsg { case _: WatchEventSpentBasic => true } val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest, paymentDb) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, progressHandler, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, randomBytes32, d, maxAttempts = 2) + val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -351,29 +384,38 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData - val failure = PermanentChannelFailure - relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) sender.send(paymentFSM, 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.expectMsgType[WatchEventSpentBasic] // this is specific to Android routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_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 - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) + } + + test("payment failed (PermanentChannelFailure)") { fixture => + testPermanentFailure(fixture, PermanentChannelFailure) + } + + test("payment failed (deprecated permanent failure)") { fixture => + import scodec.bits.HexStringSyntax + // PERM | 17 (final_expiry_too_soon) has been deprecated but older nodes might still use it. + testPermanentFailure(fixture, FailureMessageCodecs.failureMessageCodec.decode(hex"4011".bits).require.value) } test("payment succeeded") { fixture => import fixture._ - val defaultPaymentHash = randomBytes32 + val paymentPreimage = randomBytes32 + val paymentHash = Crypto.sha256(paymentPreimage) val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(id, defaultPaymentRequest.copy(paymentHash = paymentHash), paymentDb) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -382,24 +424,26 @@ class PaymentLifecycleSpec extends BaseRouterSpec { paymentFSM ! SubscribeTransitionCallBack(monitor.ref) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) - val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) + val request = SendPayment(paymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 5) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) - sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) - - val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] - assert(fee > MilliSatoshi(0)) - assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + val Some(outgoing) = paymentDb.getOutgoingPayment(id) + assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, id, Some(defaultExternalId), paymentHash, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending)) + sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, paymentPreimage)) + + val ps = eventListener.expectMsgType[PaymentSent] + assert(ps.feesPaid > 0.msat) + assert(ps.amount === defaultAmountMsat) + assert(ps.paymentHash === paymentHash) + assert(ps.paymentPreimage === paymentPreimage) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) } test("payment succeeded to a channel with fees=0") { fixture => import fixture._ import fr.acinq.eclair.randomKey - val defaultPaymentHash = randomBytes32 val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0 // \ @@ -407,24 +451,23 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val (priv_g, priv_funding_g) = (randomKey, randomKey) val (g, funding_g) = (priv_g.publicKey, priv_funding_g.publicKey) - val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil) + val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil, hex"0200") val channelId_bg = ShortChannelId(420000, 5, 0) val chan_bg = channelAnnouncement(channelId_bg, priv_b, priv_g, priv_funding_b, priv_funding_g) - val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 0, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000L) - val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) + val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) + val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) assert(Router.getDesc(channelUpdate_bg, chan_bg) === ChannelDesc(chan_bg.shortChannelId, priv_b.publicKey, priv_g.publicKey)) router ! PeerRoutingMessage(null, remoteNodeId, chan_bg) router ! PeerRoutingMessage(null, remoteNodeId, ann_g) router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_bg) router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_gb) - - // On Android we don't validate channels - //watcher.expectMsg(ValidateRequest(chan_bg)) - //watcher.send(router, ValidateResult(chan_bg, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_g)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) - //watcher.expectMsgType[WatchSpentBasic] + // not on Android: watcher.expectMsg(ValidateRequest(chan_bg)) + watcher.send(router, ValidateResult(chan_bg, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_g)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) + //not on Android: watcher.expectMsgType[WatchSpentBasic] // actual test begins - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, UUID.randomUUID(), router, TestProbe().ref)) + val progressHandler = PaymentLifecycle.DefaultPaymentProgressHandler(UUID.randomUUID(), defaultPaymentRequest.copy(targetNodeId = g), nodeParams.db.payments) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, progressHandler, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -434,7 +477,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) // we send a payment to G which is just after the - val request = SendPayment(defaultAmountMsat, defaultPaymentHash, g, maxAttempts = 5) + val request = SendPayment(defaultPaymentHash, g, FinalLegacyPayload(defaultAmountMsat, defaultExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)), maxAttempts = 5) sender.send(paymentFSM, request) // the route will be A -> B -> G where B -> G has a channel_update with fees=0 @@ -443,19 +486,20 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) - val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val paymentOK = sender.expectMsgType[PaymentSent] + val PaymentSent(_, request.paymentHash, paymentOK.paymentPreimage, PartialPayment(_, request.finalPayload.amount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent] // during the route computation the fees were treated as if they were 1msat but when sending the onion we actually put zero // NB: A -> B doesn't pay fees because it's our direct neighbor // NB: B -> G doesn't asks for fees at all - assert(fee === MilliSatoshi(0)) - assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) + assert(fee === 0.msat) + assert(fee === paymentOK.amount - request.finalPayload.amount) } test("filter errors properly") { _ => val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil - val filtered = PaymentLifecycle.transformForUser(failures) + val filtered = PaymentFailure.transformForUser(failures) assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: 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 index f67bd0e074..96f331c571 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -19,16 +19,17 @@ package fr.acinq.eclair.payment import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Protocol} import fr.acinq.eclair.payment.PaymentRequest._ +import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, _} import org.scalatest.FunSuite import scodec.DecodeResult import scodec.bits._ +import scodec.codecs.bits /** - * Created by fabrice on 15/05/17. - */ + * Created by fabrice on 15/05/17. + */ class PaymentRequestSpec extends FunSuite { @@ -38,33 +39,31 @@ class PaymentRequestSpec extends FunSuite { assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) test("check minimal unit is used") { - assert('p' === Amount.unit(MilliSatoshi(1))) - assert('p' === Amount.unit(MilliSatoshi(99))) - assert('n' === Amount.unit(MilliSatoshi(100))) - assert('p' === Amount.unit(MilliSatoshi(101))) - assert('n' === Amount.unit(Satoshi(1))) - assert('u' === Amount.unit(Satoshi(100))) - assert('n' === Amount.unit(Satoshi(101))) - assert('u' === Amount.unit(Satoshi(1155400))) - assert('m' === Amount.unit(MilliBtc(1))) - assert('m' === Amount.unit(MilliBtc(10))) - assert('m' === Amount.unit(Btc(1))) + 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 mbtc).toMilliSatoshi)) + assert('m' === Amount.unit((10 mbtc).toMilliSatoshi)) + assert('m' === Amount.unit((1 btc).toMilliSatoshi)) } test("check that we can still decode non-minimal amount encoding") { - assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000u")) - assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000000n")) - assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000000000p")) + assert(Amount.decode("1000u") === Some(100000000 msat)) + assert(Amount.decode("1000000n") === Some(100000000 msat)) + assert(Amount.decode("1000000000p") === Some(100000000 msat)) } test("data string -> bitvector") { - import scodec.bits._ assert(string2Bits("p") === bin"00001") assert(string2Bits("pz") === bin"0000100010") } test("minimal length long, left-padded to be multiple of 5") { - import scodec.bits._ assert(long2bits(0) == bin"") assert(long2bits(1) == bin"00001") assert(long2bits(42) == bin"0000101010") @@ -74,13 +73,10 @@ class PaymentRequestSpec extends FunSuite { } test("verify that padding is zero") { - import scodec.bits._ - import scodec.codecs._ 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") { @@ -101,7 +97,7 @@ class PaymentRequestSpec extends FunSuite { val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(250000000L))) + assert(pr.amount === Some(250000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -115,7 +111,7 @@ class PaymentRequestSpec extends FunSuite { val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -129,7 +125,7 @@ class PaymentRequestSpec extends FunSuite { val ref = "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf237cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqpqlhssu04sucpnz4axcv2dstmknqq6jsk2l" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lntb") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -143,15 +139,15 @@ class PaymentRequestSpec extends FunSuite { val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqj9n4evl6mr5aj9f58zp6fyjzup6ywn3x6sk8akg5v4tgn2q8g4fhx05wf6juaxu9760yp46454gpg5mtzgerlzezqcqvjnhjh8z3g2qqdhhwkj" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount === Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 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(72623859790382856L), 1, 20, 3), - ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId(217304205466536202L), 2, 30, 4) + ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId(72623859790382856L), 1 msat, 20, CltvExpiryDelta(3)), + ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId(217304205466536202L), 2 msat, 30, CltvExpiryDelta(4)) ))) assert(Protocol.writeUInt64(0x0102030405060708L, ByteOrder.BIG_ENDIAN) == hex"0102030405060708") assert(Protocol.writeUInt64(0x030405060708090aL, ByteOrder.BIG_ENDIAN) == hex"030405060708090a") @@ -159,12 +155,11 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(pr.sign(priv)) == ref) } - test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -178,7 +173,7 @@ class PaymentRequestSpec extends FunSuite { val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -188,12 +183,11 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(pr.sign(priv)) == ref) } - test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 1496314658L) assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) @@ -207,17 +201,45 @@ class PaymentRequestSpec extends FunSuite { val ref = "lnbc20m1pvjluezcqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q90qkf3gd7fcqs0ewr7t3xf72ptmc4n38evg0xhy4p64nlg7hgrmq6g997tkrvezs8afs0x0y8v4vs8thwsk6knkvdfvfa7wmhhpcsxcqw0ny48" val pr = PaymentRequest.read(ref) assert(pr.prefix == "lnbc") - assert(pr.amount == Some(MilliSatoshi(2000000000L))) + assert(pr.amount === Some(2000000000 msat)) assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(pr.timestamp == 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.minFinalCltvExpiry === Some(12)) + assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(12))) assert(pr.tags.size == 4) assert(PaymentRequest.write(pr.sign(priv)) == ref) } + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 1 and 9") { + val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl" + val pr = PaymentRequest.read(ref) + assert(pr.prefix === "lnbc") + assert(pr.amount === Some(MilliSatoshi(2500000000L))) + assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(pr.timestamp === 1496314658L) + assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(pr.description === Left("coffee beans")) + assert(pr.fallbackAddress().isEmpty) + assert(pr.features.bitmask === bin"1000000010") + assert(PaymentRequest.write(pr.sign(priv)) === ref) + } + + test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 1, 9 and 100") { + val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7" + val pr = PaymentRequest.read(ref) + assert(pr.prefix === "lnbc") + assert(pr.amount === Some(MilliSatoshi(2500000000L))) + assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(pr.timestamp === 1496314658L) + assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(pr.description === Left("coffee beans")) + assert(pr.fallbackAddress().isEmpty) + assert(pr.features.bitmask === bin"000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010") + assert(PaymentRequest.write(pr.sign(priv)) === ref) + } + test("correctly serialize/deserialize variable-length tagged fields") { val number = 123456 @@ -231,7 +253,7 @@ class PaymentRequestSpec extends FunSuite { assert(field1 == field) // Now with a payment request - val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) + val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) val serialized = PaymentRequest.write(pr) val pr1 = PaymentRequest.read(serialized) @@ -241,7 +263,7 @@ class PaymentRequestSpec extends FunSuite { test("ignore unknown tags") { val pr = PaymentRequest( prefix = "lntb", - amount = Some(MilliSatoshi(100000L)), + amount = Some(100000 msat), timestamp = System.currentTimeMillis() / 1000L, nodeId = nodeId, tags = List( @@ -253,7 +275,7 @@ class PaymentRequestSpec extends FunSuite { val serialized = PaymentRequest write pr val pr1 = PaymentRequest read serialized - val Some(unknownTag) = pr1.tags.collectFirst { case u: UnknownTag21 => u } + val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } } test("accept uppercase payment request") { @@ -265,7 +287,8 @@ class PaymentRequestSpec extends FunSuite { test("Pay 1 BTC without multiplier") { val ref = "lnbc11pdkmqhupp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2xvtsv5tc743wgctlza8k3zlpxucl7f3kvjnjptv7xz0nkaww307sdyrvgke2w8kmq7dgz4lkasfn0zvplc9aa4gp8fnhrwfjny0j59sq42x9gp" val pr = PaymentRequest.read(ref) - assert(pr.amount.contains(MilliSatoshi(100000000000L))) + assert(pr.amount === Some(100000000000L msat)) + assert(pr.features.bitmask === BitVector.empty) } test("nonreg") { @@ -326,9 +349,13 @@ class PaymentRequestSpec extends FunSuite { "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p", "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx", "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9", - "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" + "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9", + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl", + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7" ) - for (req <- requests) { assert(PaymentRequest.write(PaymentRequest.read(req)) == req) } + for (req <- requests) { + assert(PaymentRequest.write(PaymentRequest.read(req)) == req) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index 59afb080fc..299ad3058f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -20,26 +20,31 @@ import java.util.UUID import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand +import fr.acinq.eclair.payment.PaymentLifecycle.{buildCommand, buildOnion} import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, PerHopPayload, RelayTlvPayload} import fr.acinq.eclair.wire._ -import fr.acinq.eclair._ +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, TestkitBaseClass, UInt64, nodeFee, randomBytes32, randomKey} import org.scalatest.Outcome +import scodec.Attempt import scodec.bits.ByteVector import scala.concurrent.duration._ /** - * Created by PM on 29/08/2016. - */ + * Created by PM on 29/08/2016. + */ class RelayerSpec extends TestkitBaseClass { // let's reuse the existing test data + import HtlcGenerationSpec._ + import RelayerSpec._ case class FixtureParam(relayer: ActorRef, register: TestProbe, paymentHandler: TestProbe) @@ -61,15 +66,46 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) + assert(fwd.message.amount === amount_bc) + assert(fwd.message.cltvExpiry === expiry_bc) + assert(fwd.message.upstream === Right(add_ab)) + + sender.expectNoMsg(100 millis) + paymentHandler.expectNoMsg(100 millis) + } + + test("relay an htlc-add with onion tlv payload") { f => + import f._ + import fr.acinq.eclair.wire.OnionTlv._ + val sender = TestProbe() + + // Use tlv payloads for all hops (final and intermediate) + val finalPayload: Seq[PerHopPayload] = FinalTlvPayload(TlvStream[OnionTlv](AmountToForward(finalAmountMsat), OutgoingCltv(finalExpiry))) :: Nil + val (firstAmountMsat, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalAmountMsat, finalExpiry, finalPayload)) { + case ((amountMsat, expiry, currentPayloads), hop) => + val nextFee = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, amountMsat) + val payload = RelayTlvPayload(TlvStream[OnionTlv](AmountToForward(amountMsat), OutgoingCltv(expiry), OutgoingChannelId(hop.lastUpdate.shortChannelId))) + (amountMsat + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, payload +: currentPayloads) + } + val Sphinx.PacketAndSecrets(onion, _) = buildOnion(hops.map(_.nextNodeId), payloads, paymentHash) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, firstAmountMsat, paymentHash, firstExpiry, onion) + relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) + + sender.send(relayer, ForwardAdd(add_ab)) + + val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] + assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) + assert(fwd.message.amount === amount_bc) + assert(fwd.message.cltvExpiry === expiry_bc) assert(fwd.message.upstream === Right(add_ab)) sender.expectNoMsg(100 millis) @@ -81,16 +117,16 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) // we tell the relayer about channel B-C relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) // this is another channel B-C, with less balance (it will be preferred) val (channelId_bc_1, channelUpdate_bc_1) = (randomBytes32, channelUpdate_bc.copy(shortChannelId = ShortChannelId("500000x1x1"))) - relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, 49000000L)) + relayer ! LocalChannelUpdate(null, channelId_bc_1, channelUpdate_bc_1.shortChannelId, c, None, channelUpdate_bc_1, makeCommitments(channelId_bc_1, 49000000 msat)) sender.send(relayer, ForwardAdd(add_ab)) @@ -100,8 +136,8 @@ class RelayerSpec extends TestkitBaseClass { assert(fwd1.message.upstream === Right(add_ab)) // channel returns an error - val origin = Relayed(channelId_ab, originHtlcId = 42, amountMsatIn = 1100000, amountMsatOut = 1000000) - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc_1, paymentHash, HtlcValueTooHighInFlight(channelId_bc_1, UInt64(1000000000L), UInt64(1516977616L)), origin, Some(channelUpdate_bc_1), originalCommand = Some(fwd1.message)))) + val origin = Relayed(channelId_ab, originHtlcId = 42, amountIn = 1100000 msat, amountOut = 1000000 msat) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc_1, paymentHash, HtlcValueTooHighInFlight(channelId_bc_1, UInt64(1000000000L), 1516977616L msat), origin, Some(channelUpdate_bc_1), originalCommand = Some(fwd1.message)))) // second try val fwd2 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] @@ -109,7 +145,7 @@ class RelayerSpec extends TestkitBaseClass { assert(fwd2.message.upstream === Right(add_ab)) // failure again - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcValueTooHighInFlight(channelId_bc, UInt64(1000000000L), UInt64(1516977616L)), origin, Some(channelUpdate_bc), originalCommand = Some(fwd2.message)))) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcValueTooHighInFlight(channelId_bc, UInt64(1000000000L), 1516977616L msat), origin, Some(channelUpdate_bc), originalCommand = Some(fwd2.message)))) // the relayer should give up val fwdFail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] @@ -121,14 +157,29 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } + test("relay an htlc-add at the final node to the payment handler") { f => + import f._ + val sender = TestProbe() + + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops.take(1), FinalLegacyPayload(finalAmountMsat, finalExpiry)) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + sender.send(relayer, ForwardAdd(add_ab)) + + val htlc = paymentHandler.expectMsgType[UpdateAddHtlc] + assert(htlc === add_ab) + + sender.expectNoMsg(100 millis) + register.expectNoMsg(100 millis) + } + test("fail to relay an htlc-add when we have no channel_update for the next channel") { f => import f._ val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) sender.send(relayer, ForwardAdd(add_ab)) @@ -146,9 +197,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -173,8 +224,8 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // check that payments are sent properly - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -189,8 +240,8 @@ class RelayerSpec extends TestkitBaseClass { // now tell the relayer that the channel is down and try again relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) - val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, randomBytes32, hops) - val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) + val (cmd1, _) = buildCommand(UUID.randomUUID(), randomBytes32, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) + val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amount, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) sender.send(relayer, ForwardAdd(add_ab1)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message @@ -206,9 +257,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = Announcements.makeChannelFlags(Announcements.isNode1(channelUpdate_bc.channelFlags), enable = false)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc_disabled, makeCommitments(channelId_bc)) @@ -227,9 +278,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc with an invalid onion (hmac) - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -243,21 +294,68 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } + test("fail to relay an htlc-add when the onion payload is missing data") { f => + import f._ + import fr.acinq.eclair.wire.OnionTlv._ + + // B is not the last hop and receives an onion missing some routing information. + val invalidPayloads_bc = Seq( + (InvalidOnionPayload(UInt64(2), 0), TlvStream[OnionTlv](OutgoingChannelId(channelUpdate_bc.shortChannelId), OutgoingCltv(expiry_bc))), // Missing forwarding amount. + (InvalidOnionPayload(UInt64(4), 0), TlvStream[OnionTlv](OutgoingChannelId(channelUpdate_bc.shortChannelId), AmountToForward(amount_bc))), // Missing cltv expiry. + (InvalidOnionPayload(UInt64(6), 0), TlvStream[OnionTlv](AmountToForward(amount_bc), OutgoingCltv(expiry_bc)))) // Missing channel id. + val payload_cd = TlvStream[OnionTlv](OutgoingChannelId(channelUpdate_cd.shortChannelId), AmountToForward(amount_cd), OutgoingCltv(expiry_cd)) + + val sender = TestProbe() + relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) + + for ((expectedErr, invalidPayload_bc) <- invalidPayloads_bc) { + val onion = buildTlvOnion(Seq(b, c), Seq(invalidPayload_bc, payload_cd), paymentHash) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) + sender.send(relayer, ForwardAdd(add_ab)) + + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(expectedErr), commit = true))) + register.expectNoMsg(100 millis) + paymentHandler.expectNoMsg(100 millis) + } + } + + test("fail to relay an htlc-add when variable length onion is disabled") { f => + import f._ + import fr.acinq.eclair.wire.OnionTlv._ + + val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(globalFeatures = ByteVector.empty), register.ref, paymentHandler.ref)) + val sender = TestProbe() + relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) + + val payload_bc = TlvStream[OnionTlv](OutgoingChannelId(channelUpdate_bc.shortChannelId), AmountToForward(amount_bc), OutgoingCltv(expiry_bc)) + val payload_cd = TlvStream[OnionTlv](OutgoingChannelId(channelUpdate_cd.shortChannelId), AmountToForward(amount_cd), OutgoingCltv(expiry_cd)) + + val onion = buildTlvOnion(Seq(b, c), Seq(payload_bc, payload_cd), paymentHash) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) + sender.send(relayer, ForwardAdd(add_ab)) + + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(InvalidRealm), commit = true))) + register.expectNoMsg(100 millis) + paymentHandler.expectNoMsg(100 millis) + } + test("fail to relay an htlc-add when amount is below the next hop's requirements") { f => import f._ val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(UUID.randomUUID(), channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) + val finalPayload = FinalLegacyPayload(channelUpdate_bc.htlcMinimumMsat - (1 msat), finalExpiry) + val zeroFeeHops = hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0 msat, feeProportionalMillionths = 0))) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, zeroFeeHops, finalPayload) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(fail.id === add_ab.id) - assert(fail.reason == Right(AmountBelowMinimum(cmd.amountMsat, channelUpdate_bc))) + assert(fail.reason == Right(AmountBelowMinimum(cmd.amount, channelUpdate_bc))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -267,10 +365,10 @@ class RelayerSpec extends TestkitBaseClass { import f._ val sender = TestProbe() - val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) + val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = CltvExpiryDelta(0)))) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -288,9 +386,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2))) - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -309,9 +407,9 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc with a wrong expiry - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount - (1 msat), cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -330,9 +428,9 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), paymentHash, hops1, FinalLegacyPayload(finalAmountMsat, finalExpiry)) // and then manually build an htlc with a wrong expiry - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry - 1, cmd.onion) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry - CltvExpiryDelta(1), cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -345,20 +443,42 @@ class RelayerSpec extends TestkitBaseClass { paymentHandler.expectNoMsg(100 millis) } + test("fail an htlc-add at the final node when the onion payload is missing data") { f => + import f._ + import fr.acinq.eclair.wire.OnionTlv._ + + // B is the last hop and receives an onion missing some payment information. + val invalidFinalPayloads = Seq( + (InvalidOnionPayload(UInt64(2), 0), TlvStream[OnionTlv](OutgoingCltv(expiry_bc))), // Missing forwarding amount. + (InvalidOnionPayload(UInt64(4), 0), TlvStream[OnionTlv](AmountToForward(amount_bc)))) // Missing cltv expiry. + + val sender = TestProbe() + + for ((expectedErr, invalidFinalPayload) <- invalidFinalPayloads) { + val onion = buildTlvOnion(Seq(b), Seq(invalidFinalPayload), paymentHash) + val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) + sender.send(relayer, ForwardAdd(add_ab)) + + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(expectedErr), commit = true))) + register.expectNoMsg(100 millis) + paymentHandler.expectNoMsg(100 millis) + } + } + test("correctly translates errors returned by channel when attempting to add an htlc") { f => import f._ val sender = TestProbe() val paymentHash = randomBytes32 - val origin = Relayed(channelId_ab, originHtlcId = 42, amountMsatIn = 1100000, amountMsatOut = 1000000) + val origin = Relayed(channelId_ab, originHtlcId = 42, amountIn = 1100000 msat, amountOut = 1000000 msat) - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooSmall(channelId_bc, 100, 0, 0), origin, Some(channelUpdate_bc), None))) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooSmall(channelId_bc, CltvExpiry(100), CltvExpiry(0), 0), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ExpiryTooSoon(channelUpdate_bc))) - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooBig(channelId_bc, 100, 200, 0), origin, Some(channelUpdate_bc), None))) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ExpiryTooBig(channelId_bc, CltvExpiry(100), CltvExpiry(200), 0), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ExpiryTooFar)) - sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountMsatOut, 100, 0, 0), origin, Some(channelUpdate_bc), None))) + sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountOut, 100 sat, 0 sat, 0 sat), origin, Some(channelUpdate_bc), None))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(TemporaryChannelFailure(channelUpdate_bc))) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = 2) @@ -383,9 +503,9 @@ class RelayerSpec extends TestkitBaseClass { system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket) val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = ByteVector32.Zeroes) - val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) + val origin = Relayed(channelId_ab, 150, 11000000 msat, 10000000 msat) sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc)) val fwd = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] @@ -393,7 +513,7 @@ class RelayerSpec extends TestkitBaseClass { assert(fwd.message.id === origin.originHtlcId) val paymentRelayed = eventListener.expectMsgType[PaymentRelayed] - assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0)) + assert(paymentRelayed.copy(timestamp = 0) === PaymentRelayed(origin.amountIn, origin.amountOut, add_bc.paymentHash, channelId_ab, channelId_bc, timestamp = 0)) } test("relay an htlc-fail") { f => @@ -401,9 +521,9 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000 msat, paymentHash = ByteVector32.Zeroes, CltvExpiry(4200), onionRoutingPacket = TestConstants.emptyOnionPacket) val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) - val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) + val origin = Relayed(channelId_ab, 150, 11000000 msat, 10000000 msat) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] @@ -414,33 +534,50 @@ class RelayerSpec extends TestkitBaseClass { test("get usable balances") { f => import f._ val sender = TestProbe() - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000, 300000)) - relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000, -5000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat)) + relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat)) sender.send(relayer, GetUsableBalances) val usableBalances1 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances1.size === 2) - assert(usableBalances1.head.canSendMsat === 0 && usableBalances1.head.canReceiveMsat === 300000 && usableBalances1.head.shortChannelId == channelUpdate_ab.shortChannelId) - assert(usableBalances1.last.canReceiveMsat === 0 && usableBalances1.last.canSendMsat === 400000 && usableBalances1.last.shortChannelId == channelUpdate_bc.shortChannelId) + assert(usableBalances1.head.canSend === 0.msat && usableBalances1.head.canReceive === 300000.msat && usableBalances1.head.shortChannelId == channelUpdate_ab.shortChannelId) + assert(usableBalances1.last.canReceive === 0.msat && usableBalances1.last.canSend === 400000.msat && usableBalances1.last.shortChannelId == channelUpdate_bc.shortChannelId) - relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, 0, makeCommitments(channelId_bc, 200000, 500000)) + relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, 0 msat, makeCommitments(channelId_bc, 200000 msat, 500000 msat)) sender.send(relayer, GetUsableBalances) val usableBalances2 = sender.expectMsgType[Iterable[UsableBalances]] - assert(usableBalances2.last.canReceiveMsat === 500000 && usableBalances2.last.canSendMsat === 200000) + assert(usableBalances2.last.canReceive === 500000.msat && usableBalances2.last.canSend === 200000.msat) - relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, 0, makeCommitments(channelId_ab, 100000, 200000)) + relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, 0 msat, makeCommitments(channelId_ab, 100000 msat, 200000 msat)) relayer ! LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c) sender.send(relayer, GetUsableBalances) val usableBalances3 = sender.expectMsgType[Iterable[UsableBalances]] - assert(usableBalances3.size === 1 && usableBalances3.head.canSendMsat === 100000) + assert(usableBalances3.size === 1 && usableBalances3.head.canSend === 100000.msat) - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = 2), makeCommitments(channelId_ab, 100000, 200000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = 2), makeCommitments(channelId_ab, 100000 msat, 200000 msat)) sender.send(relayer, GetUsableBalances) val usableBalances4 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances4.isEmpty) - relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000, 200000)) + relayer ! LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat)) sender.send(relayer, GetUsableBalances) val usableBalances5 = sender.expectMsgType[Iterable[UsableBalances]] assert(usableBalances5.size === 1) } } + +object RelayerSpec { + + /** Build onion from arbitrary tlv stream (potentially invalid). */ + def buildTlvOnion(nodes: Seq[PublicKey], payloads: Seq[TlvStream[OnionTlv]], associatedData: ByteVector32): OnionRoutingPacket = { + require(nodes.size == payloads.size) + val sessionKey = randomKey + val payloadsBin: Seq[ByteVector] = payloads + .map(OnionCodecs.tlvPerHopPayloadCodec.encode) + .map { + case Attempt.Successful(bitVector) => bitVector.toByteVector + case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") + } + Sphinx.PaymentPacket.create(sessionKey, nodes, payloadsBin, associatedData).packet + } + +} \ No newline at end of file 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 0667d2a7bd..5256d34d16 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 @@ -27,7 +27,9 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{ShortChannelId, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, randomKey} +import org.json4s +import org.json4s.JsonAST.{JString, JValue} import org.scalatest.FunSuite import scodec.bits.ByteVector @@ -35,8 +37,8 @@ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ class AnnouncementsBatchValidationSpec extends FunSuite { @@ -45,8 +47,8 @@ class AnnouncementsBatchValidationSpec extends FunSuite { ignore("validate a batch of announcements") { import scala.concurrent.ExecutionContext.Implicits.global - implicit val system = ActorSystem() - implicit val sttpBackend = OkHttpFutureBackend() + implicit val system = ActorSystem("test") + implicit val sttpBackend = OkHttpFutureBackend() implicit val extendedBitcoinClient = new ExtendedBitcoinClient(new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 18332)) val channels = for (i <- 0 until 50) yield { @@ -76,15 +78,20 @@ object AnnouncementsBatchValidationSpec { case class SimulatedChannel(node1Key: PrivateKey, node2Key: PrivateKey, node1FundingKey: PrivateKey, node2FundingKey: PrivateKey, amount: Satoshi, fundingTx: Transaction, fundingOutputIndex: Int) - def generateBlocks(numBlocks: Int)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext) = - Await.result(extendedBitcoinClient.rpcClient.invoke("generate", numBlocks), 10 seconds) + def generateBlocks(numBlocks: Int)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext) = { + val generatedF = for { + JString(address) <- extendedBitcoinClient.rpcClient.invoke("getnewaddress") + _ <- extendedBitcoinClient.rpcClient.invoke("generatetoaddress", numBlocks, address) + } yield () + Await.result(generatedF, 10 seconds) + } def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): SimulatedChannel = { val node1Key = randomKey val node2Key = randomKey val node1BitcoinKey = randomKey val node2BitcoinKey = randomKey - val amount = Satoshi(1000000) + val amount = 1000000 sat // first we publish the funding tx val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey))) @@ -104,6 +111,6 @@ object AnnouncementsBatchValidationSpec { } def makeChannelUpdate(c: SimulatedChannel, shortChannelId: ShortChannelId): ChannelUpdate = - Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, c.node1Key, c.node2Key.publicKey, shortChannelId, 10, 1000, 10, 100, 500000000L) + Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, c.node1Key, c.node2Key.publicKey, shortChannelId, CltvExpiryDelta(10), 1000 msat, 10 msat, 100, 500000000 msat) } \ No newline at end of file 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 e361d10826..1b2a34378c 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 @@ -25,8 +25,8 @@ import org.scalatest.FunSuite import scodec.bits._ /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ class AnnouncementsSpec extends FunSuite { @@ -48,14 +48,14 @@ class AnnouncementsSpec extends FunSuite { } ignore("create valid signed node announcement") { - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.globalFeatures) assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } ignore("create valid signed channel update announcement") { - val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth, 500000000L) + val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimum, Alice.nodeParams.feeBase, Alice.nodeParams.feeProportionalMillionth, MilliSatoshi(500000000L)) assert(checkSig(ann, Alice.nodeParams.nodeId)) assert(checkSig(ann, randomKey.publicKey) === false) } @@ -66,10 +66,10 @@ class AnnouncementsSpec extends FunSuite { // NB: node1 < node2 (public keys) assert(isNode1(node1_priv.publicKey, node2_priv.publicKey)) assert(!isNode1(node2_priv.publicKey, node1_priv.publicKey)) - val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = true) - val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = false) - val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = true) - val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), 0, 0, 0, 0, 500000000L, enable = false) + val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), CltvExpiryDelta(0), 0 msat, 0 msat, 0, 500000000 msat, enable = true) + val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, ShortChannelId(0), CltvExpiryDelta(0), 0 msat, 0 msat, 0, 500000000 msat, enable = false) + val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), CltvExpiryDelta(0), 0 msat, 0 msat, 0, 500000000 msat, enable = true) + val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, ShortChannelId(0), CltvExpiryDelta(0), 0 msat, 0 msat, 0, 500000000 msat, enable = false) assert(channelUpdate1.channelFlags == 0) // ....00 assert(channelUpdate1_disabled.channelFlags == 2) // ....10 assert(channelUpdate2.channelFlags == 1) // ....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 647bb71f8f..253fe26796 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 @@ -20,7 +20,7 @@ import akka.actor.ActorRef import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, ByteVector32, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} import fr.acinq.eclair.crypto.LocalKeyManager @@ -30,15 +30,15 @@ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import fr.acinq.eclair.{TestkitBaseClass, randomKey, _} import org.scalatest.Outcome -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, HexStringSyntax} import scala.concurrent.duration._ /** - * Base class for router testing. - * It is re-used in payment FSM tests - * Created by PM on 29/08/2016. - */ + * Base class for router testing. + * It is re-used in payment FSM tests + * Created by PM on 29/08/2016. + */ abstract class BaseRouterSpec extends TestkitBaseClass { @@ -55,12 +55,12 @@ abstract class BaseRouterSpec extends TestkitBaseClass { 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) - val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil) - val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil) - val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil) - val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil) - val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil) + val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, hex"0200") + val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, hex"") + val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200") + val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, hex"00") + val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, hex"00") + val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, hex"00") val channelId_ab = ShortChannelId(420000, 1, 0) val channelId_bc = ShortChannelId(420000, 2, 0) @@ -78,14 +78,14 @@ abstract class BaseRouterSpec extends TestkitBaseClass { 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 channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L) - val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) - val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, cltvExpiryDelta = 5, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000L) - val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) - val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, cltvExpiryDelta = 3, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L) - val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) - val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L) + val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat) + val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000 msat) + val channelUpdate_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat) + val channelUpdate_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = 500000000 msat) + val channelUpdate_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat) + val channelUpdate_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000 msat) + val channelUpdate_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) + val channelUpdate_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) override def withFixture(test: OneArgTest): Outcome = { // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala deleted file mode 100644 index 4cd6345fd4..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesExSpec.scala +++ /dev/null @@ -1,34 +0,0 @@ -package fr.acinq.eclair.router - -import fr.acinq.bitcoin.Block -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRangeEx -import org.scalatest.FunSuite - -import scala.collection.immutable.SortedMap -import scala.util.Random - -class ChannelRangeQueriesExSpec extends FunSuite { - val random = new Random() - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds - val timestamps = shortChannelIds.map(id => id -> random.nextInt(400000).toLong).toMap - - test("create `reply_channel_range_ex` messages (uncompressed format)") { - val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamps(id) == decoded(id)) - } - - test("create `reply_channel_range_ex` messages (zlib format)") { - val blocks = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(400000, 20000, shortChannelIds, id => timestamps(id), ChannelRangeQueries.ZLIB_FORMAT) - val replies = blocks.map(block => ReplyChannelRangeEx(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIdAndTimestamps)) - var decoded = SortedMap.empty[ShortChannelId, Long] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueriesEx.decodeShortChannelIdAndTimestamps(reply.data)._2) - assert(decoded.keySet == shortChannelIds) - shortChannelIds.foreach(id => timestamps(id) == decoded(id)) - } - -} 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 4aa74b7999..0db728df88 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,63 +16,115 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Block -import fr.acinq.eclair.ShortChannelId -import fr.acinq.eclair.wire.ReplyChannelRange +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ +import fr.acinq.eclair.{LongToBtcAmount, randomKey} import org.scalatest.FunSuite +import scodec.bits.ByteVector -import scala.collection.{SortedSet, immutable} +import scala.collection.immutable.SortedMap +import scala.compat.Platform class ChannelRangeQueriesSpec extends FunSuite { - import ChannelRangeQueriesSpec._ - - test("create `reply_channel_range` messages (uncompressed format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ ChannelRangeQueries.decodeShortChannelIds(reply.data)._2) - assert(decoded == shortChannelIds) - } - test("create `reply_channel_range` messages (ZLIB format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, false) = ChannelRangeQueries.decodeShortChannelIds(reply.data) - ids - }) - assert(decoded == shortChannelIds) - } + test("ask for update test") { + // they don't provide anything => we always ask for the update + assert(Router.shouldRequestUpdate(0, 0, None, None)) + assert(Router.shouldRequestUpdate(Int.MaxValue, 12345, None, None)) + + // their update is older => don't ask + val now = Platform.currentTime / 1000 + assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), None)) + assert(!Router.shouldRequestUpdate(now, 0, Some(now - 1), Some(12345))) + assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), None)) + assert(!Router.shouldRequestUpdate(now, 12344, Some(now - 1), Some(12345))) + + // their update is newer but stale => don't ask + val old = now - 4 * 2016 * 24 * 3600 + assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), None)) + assert(!Router.shouldRequestUpdate(old - 1, 0, Some(old), Some(12345))) + assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), None)) + assert(!Router.shouldRequestUpdate(old - 1, 12344, Some(old), Some(12345))) + + // their update is newer but with the same checksum, and ours is stale or about to be => ask (we want to renew our update) + assert(Router.shouldRequestUpdate(old, 12345, Some(now), Some(12345))) + + // their update is newer but with the same checksum => don't ask + assert(!Router.shouldRequestUpdate(now - 1, 12345, Some(now), Some(12345))) - test("create `reply_channel_range` messages (GZIP format)") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, shortChannelIds, ChannelRangeQueries.ZLIB_FORMAT, useGzip = true) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (ChannelRangeQueries.ZLIB_FORMAT, ids, true) = ChannelRangeQueries.decodeShortChannelIds(reply.data) - ids - }) - assert(decoded == shortChannelIds) + // their update is newer with a different checksum => always ask + assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), None)) + assert(Router.shouldRequestUpdate(now - 1, 0, Some(now), Some(12345))) + assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), None)) + assert(Router.shouldRequestUpdate(now - 1, 12344, Some(now), Some(12345))) + + // they just provided a 0 checksum => don't ask + assert(!Router.shouldRequestUpdate(0, 0, None, Some(0))) + assert(!Router.shouldRequestUpdate(now, 1234, None, Some(0))) + + // they just provided a checksum that is the same as us => don't ask + assert(!Router.shouldRequestUpdate(now, 1234, None, Some(1234))) + + // they just provided a different checksum that is the same as us => ask + assert(Router.shouldRequestUpdate(now, 1234, None, Some(1235))) } - test("create empty `reply_channel_range` message") { - val blocks = ChannelRangeQueries.encodeShortChannelIds(400000, 20000, SortedSet.empty[ShortChannelId], ChannelRangeQueries.ZLIB_FORMAT, useGzip = false) - val replies = blocks.map(block => ReplyChannelRange(Block.RegtestGenesisBlock.blockId, block.firstBlock, block.numBlocks, 1, block.shortChannelIds)) - var decoded = Set.empty[ShortChannelId] - replies.foreach(reply => decoded = decoded ++ { - val (format, ids, false) = ChannelRangeQueries.decodeShortChannelIds(reply.data) - ids - }) - assert(decoded.isEmpty) + test("compute checksums") { + assert(Router.crc32c(ByteVector.fromValidHex("00" * 32)) == 0x8a9136aaL) + assert(Router.crc32c(ByteVector.fromValidHex("FF" * 32)) == 0x62a8ab43L) + assert(Router.crc32c(ByteVector((0 to 31).map(_.toByte))) == 0x46dd794eL) + assert(Router.crc32c(ByteVector((31 to 0 by -1).map(_.toByte))) == 0x113fdb5cL) } -} -object ChannelRangeQueriesSpec { - lazy val shortChannelIds: immutable.SortedSet[ShortChannelId] = (for { - block <- 400000 to 420000 - txindex <- 0 to 5 - outputIndex <- 0 to 1 - } yield ShortChannelId(block, txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + test("compute flag tests") { + + val now = Platform.currentTime / 1000 + + val a = randomKey.publicKey + val b = randomKey.publicKey + val ab = RouteCalculationSpec.makeChannel(123466L, a, b) + val (ab1, uab1) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId1, ab.nodeId2, 0 msat, 0, timestamp = now) + val (ab2, uab2) = RouteCalculationSpec.makeUpdateShort(ab.shortChannelId, ab.nodeId2, ab.nodeId1, 0 msat, 0, timestamp = now) + + val c = randomKey.publicKey + val d = randomKey.publicKey + val cd = RouteCalculationSpec.makeChannel(451312L, c, d) + val (cd1, ucd1) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId1, cd.nodeId2, 0 msat, 0, timestamp = now) + val (_, ucd2) = RouteCalculationSpec.makeUpdateShort(cd.shortChannelId, cd.nodeId2, cd.nodeId1, 0 msat, 0, timestamp = now) + + val e = randomKey.publicKey + val f = randomKey.publicKey + val ef = RouteCalculationSpec.makeChannel(167514L, e, f) + + val channels = SortedMap( + ab.shortChannelId -> PublicChannel(ab, ByteVector32.Zeroes, 0 sat, Some(uab1), Some(uab2)), + cd.shortChannelId -> PublicChannel(cd, ByteVector32.Zeroes, 0 sat, Some(ucd1), None) + ) + + import fr.acinq.eclair.wire.QueryShortChannelIdsTlv.QueryFlagType._ + + assert(Router.getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L))) + + // no extended info but we know the channel: we ask for the updates + assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(Router.computeFlag(channels)(ab.shortChannelId, None, None, 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(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), true) === 0) + // different checksums, newer timestamps: we ask for the updates + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), 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(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), true) === 0) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), true) === 0) + assert(Router.computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), true) === 0) + + // missing channel update: we ask for it + assert(Router.computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + + // unknown channel: we ask everything + assert(Router.computeFlag(channels)(ef.shortChannelId, None, None, false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(Router.computeFlag(channels)(ef.shortChannelId, None, None, true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + } } 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 85c0e2ce82..a8808316d4 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 @@ -17,10 +17,10 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.wire.ChannelUpdate +import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId} import org.scalatest.FunSuite import scodec.bits._ @@ -37,24 +37,24 @@ class GraphSpec extends FunSuite { ) /** - * /--> D --\ - * A --> B --> C - * \-> E/ - * - * @return - */ + * /--> D --\ + * A --> B --> C + * \-> E/ + * + * @return + */ def makeTestGraph() = { val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, a, d, 0, 0), - makeUpdate(4L, d, c, 0, 0), - makeUpdate(5L, c, e, 0, 0), - makeUpdate(6L, b, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, a, d, 0 msat, 0), + makeUpdate(4L, d, c, 0 msat, 0), + makeUpdate(5L, c, e, 0 msat, 0), + makeUpdate(6L, b, e, 0 msat, 0) ) - DirectedGraph.makeGraph(updates.toMap) + DirectedGraph().addEdges(updates) } test("instantiate a graph, with vertices and then add edges") { @@ -72,11 +72,11 @@ class GraphSpec extends FunSuite { assert(otherGraph.vertexSet().size === 5) // add some edges to the graph - val (descAB, updateAB) = makeUpdate(1L, a, b, 0, 0) - val (descBC, updateBC) = makeUpdate(2L, b, c, 0, 0) - val (descAD, updateAD) = makeUpdate(3L, a, d, 0, 0) - val (descDC, updateDC) = makeUpdate(4L, d, c, 0, 0) - val (descCE, updateCE) = makeUpdate(5L, c, e, 0, 0) + val (descAB, updateAB) = makeUpdate(1L, a, b, 0 msat, 0) + val (descBC, updateBC) = makeUpdate(2L, b, c, 0 msat, 0) + val (descAD, updateAD) = makeUpdate(3L, a, d, 0 msat, 0) + val (descDC, updateDC) = makeUpdate(4L, d, c, 0 msat, 0) + val (descCE, updateCE) = makeUpdate(5L, c, e, 0 msat, 0) val graphWithEdges = graph .addEdge(descAB, updateAB) @@ -98,12 +98,12 @@ class GraphSpec extends FunSuite { test("instantiate a graph adding edges only") { - val edgeAB = edgeFromDesc(makeUpdate(1L, a, b, 0, 0)) - val (descBC, updateBC) = makeUpdate(2L, b, c, 0, 0) - val (descAD, updateAD) = makeUpdate(3L, a, d, 0, 0) - val (descDC, updateDC) = makeUpdate(4L, d, c, 0, 0) - val (descCE, updateCE) = makeUpdate(5L, c, e, 0, 0) - val (descBE, updateBE) = makeUpdate(6L, b, e, 0, 0) + val edgeAB = edgeFromDesc(makeUpdate(1L, a, b, 0 msat, 0)) + val (descBC, updateBC) = makeUpdate(2L, b, c, 0 msat, 0) + val (descAD, updateAD) = makeUpdate(3L, a, d, 0 msat, 0) + val (descDC, updateDC) = makeUpdate(4L, d, c, 0 msat, 0) + val (descCE, updateCE) = makeUpdate(5L, c, e, 0 msat, 0) + val (descBE, updateBE) = makeUpdate(6L, b, e, 0 msat, 0) val graph = DirectedGraph(edgeAB) .addEdge(descAD, updateAD) @@ -121,10 +121,10 @@ class GraphSpec extends FunSuite { test("containsEdge should return true if the graph contains that edge, false otherwise") { val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ) val graph = DirectedGraph().addEdges(updates) @@ -144,10 +144,10 @@ class GraphSpec extends FunSuite { val graph = makeTestGraph() - val (descBE, _) = makeUpdate(6L, b, e, 0, 0) - val (descCE, _) = makeUpdate(5L, c, e, 0, 0) - val (descAD, _) = makeUpdate(3L, a, d, 0, 0) - val (descDC, _) = makeUpdate(4L, d, c, 0, 0) + val (descBE, _) = makeUpdate(6L, b, e, 0 msat, 0) + val (descCE, _) = makeUpdate(5L, c, e, 0 msat, 0) + val (descAD, _) = makeUpdate(3L, a, d, 0 msat, 0) + val (descDC, _) = makeUpdate(4L, d, c, 0 msat, 0) assert(graph.edgeSet().size === 6) @@ -161,15 +161,15 @@ class GraphSpec extends FunSuite { val withoutAnyIncomingEdgeInE = graph.removeEdges(Seq(descBE, descCE)) assert(withoutAnyIncomingEdgeInE.containsVertex(e)) - assert(withoutAnyIncomingEdgeInE.edgesOf(e).size == 0) + assert(withoutAnyIncomingEdgeInE.edgesOf(e).isEmpty) } test("should get an edge given two vertices") { // contains an edge A --> B val updates = Seq( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0) ) val graph = DirectedGraph().addEdges(updates) @@ -199,19 +199,19 @@ class GraphSpec extends FunSuite { assert(graph.edgesOf(a).size == 2) //now add a new edge a -> b but with a different channel update and a different ShortChannelId - val newEdgeForNewChannel = edgeFromDesc(makeUpdate(15L, a, b, 20, 0)) + val newEdgeForNewChannel = edgeFromDesc(makeUpdate(15L, a, b, 20 msat, 0)) val mutatedGraph = graph.addEdge(newEdgeForNewChannel) assert(mutatedGraph.edgesOf(a).size == 3) //if the ShortChannelId is the same we replace the edge and the update, this edge have an update with a different 'feeBaseMsat' - val edgeForTheSameChannel = edgeFromDesc(makeUpdate(15L, a, b, 30, 0)) + val edgeForTheSameChannel = edgeFromDesc(makeUpdate(15L, a, b, 30 msat, 0)) 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) + assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.update.feeBaseMsat === 30.msat) } test("remove a vertex with incoming edges and check those edges are removed too") { @@ -234,11 +234,11 @@ class GraphSpec extends FunSuite { def edgeFromDesc(tuple: (ChannelDesc, ChannelUpdate)): GraphEdge = GraphEdge(tuple._1, tuple._2) def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = { - makeUpdate(shortChannelId, a, b, 0, 0)._1 + makeUpdate(shortChannelId, a, b, 0 msat, 0)._1 } def edgeFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): GraphEdge = { - edgeFromDesc(makeUpdate(shortChannelId, a, b, 0, 0)) + edgeFromDesc(makeUpdate(shortChannelId, a, b, 0 msat, 0)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala new file mode 100644 index 0000000000..78b6945818 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/NetworkStatsSpec.scala @@ -0,0 +1,95 @@ +/* + * 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.router + +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.Satoshi +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32, randomBytes64, randomKey} +import org.scalatest.FunSuite + +import scala.util.Random + +/** + * Created by t-bast on 30/08/2019. + */ + +class NetworkStatsSpec extends FunSuite { + + import NetworkStatsSpec._ + + test("network data missing") { + assert(NetworkStats(Nil) === None) + assert(NetworkStats(Seq( + PublicChannel(fakeChannelAnnouncement(randomKey.publicKey, randomKey.publicKey), randomBytes32, 10 sat, None, None), + PublicChannel(fakeChannelAnnouncement(randomKey.publicKey, randomKey.publicKey), randomBytes32, 15 sat, None, None) + )) === None) + } + + test("small network") { + val nodes = Seq.fill(6)(randomKey.publicKey) + val channels = Seq( + PublicChannel(fakeChannelAnnouncement(nodes(0), nodes(1)), randomBytes32, 10 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(10), 10 msat, 10)), Some(fakeChannelUpdate2(CltvExpiryDelta(15), 15 msat, 15))), + PublicChannel(fakeChannelAnnouncement(nodes(1), nodes(2)), randomBytes32, 20 sat, None, Some(fakeChannelUpdate2(CltvExpiryDelta(25), 25 msat, 25))), + PublicChannel(fakeChannelAnnouncement(nodes(2), nodes(3)), randomBytes32, 30 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(30), 30 msat, 30)), Some(fakeChannelUpdate2(CltvExpiryDelta(35), 35 msat, 35))), + PublicChannel(fakeChannelAnnouncement(nodes(3), nodes(4)), randomBytes32, 40 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(40), 40 msat, 40)), None), + PublicChannel(fakeChannelAnnouncement(nodes(4), nodes(5)), randomBytes32, 50 sat, Some(fakeChannelUpdate1(CltvExpiryDelta(50), 50 msat, 50)), Some(fakeChannelUpdate2(CltvExpiryDelta(55), 55 msat, 55))) + ) + val Some(stats) = NetworkStats(channels) + assert(stats.channels === 5) + assert(stats.nodes === 6) + assert(stats.capacity === Stats(30 sat, 12 sat, 14 sat, 20 sat, 40 sat, 46 sat, 48 sat)) + assert(stats.cltvExpiryDelta === Stats(CltvExpiryDelta(32), CltvExpiryDelta(11), CltvExpiryDelta(13), CltvExpiryDelta(22), CltvExpiryDelta(42), CltvExpiryDelta(51), CltvExpiryDelta(53))) + assert(stats.feeBase === Stats(32 msat, 11 msat, 13 msat, 22 msat, 42 msat, 51 msat, 53 msat)) + assert(stats.feeProportional === Stats(32, 11, 13, 22, 42, 51, 53)) + } + + test("intermediate network") { + val rand = new Random() + val nodes = Seq.fill(100)(randomKey.publicKey) + val channels = Seq.fill(500)(PublicChannel( + fakeChannelAnnouncement(nodes(rand.nextInt(nodes.size)), nodes(rand.nextInt(nodes.size))), + randomBytes32, + Satoshi(1000 + rand.nextInt(10000)), + Some(fakeChannelUpdate1(CltvExpiryDelta(12 + rand.nextInt(144)), MilliSatoshi(21000 + rand.nextInt(79000)), rand.nextInt(1000))), + Some(fakeChannelUpdate2(CltvExpiryDelta(12 + rand.nextInt(144)), MilliSatoshi(21000 + rand.nextInt(79000)), rand.nextInt(1000))) + )) + val Some(stats) = NetworkStats(channels) + assert(stats.channels === 500) + assert(stats.nodes <= 100) + assert(1000.sat <= stats.capacity.median && stats.capacity.median <= 11000.sat) + assert(stats.feeBase.percentile10 >= 21000.msat) + assert(stats.feeProportional.median <= 1000) + } + +} + +object NetworkStatsSpec { + + def fakeChannelAnnouncement(local: PublicKey, remote: PublicKey): ChannelAnnouncement = { + Announcements.makeChannelAnnouncement(randomBytes32, ShortChannelId(42), local, remote, randomKey.publicKey, randomKey.publicKey, randomBytes64, randomBytes64, randomBytes64, randomBytes64) + } + + def fakeChannelUpdate1(cltv: CltvExpiryDelta, feeBase: MilliSatoshi, feeProportional: Long): ChannelUpdate = { + ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(42), 0, 0, 0, cltv, 1 msat, feeBase, feeProportional, None) + } + + def fakeChannelUpdate2(cltv: CltvExpiryDelta, feeBase: MilliSatoshi, feeProportional: Long): ChannelUpdate = { + ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(42), 0, 0, 1, cltv, 1 msat, feeBase, feeProportional, None) + } + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/PruningSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/PruningSpec.scala deleted file mode 100644 index a944b76284..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/PruningSpec.scala +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2018 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 akka.actor.{Actor, ActorRef, Props} -import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction, TxOut} -import fr.acinq.eclair.TestConstants.Alice -import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, TxCoordinates} -import org.scalatest.{BeforeAndAfterAll, Outcome} -import scodec.bits.ByteVector - -import scala.collection.{SortedSet, immutable} -import scala.concurrent.duration._ - -class PruningSpec extends TestkitBaseClass with BeforeAndAfterAll { - import PruningSpec._ - - val txid = ByteVector32.fromValidHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - val remoteNodeId = PrivateKey(ByteVector32.fromValidHex("01" * 32)).publicKey - - val startHeight = 400000 - 25 * 2016 - val shortChannelIds: immutable.SortedSet[ShortChannelId] = (for { - block <- startHeight to startHeight + 50 * 50 by 50 - txindex <- 0 to 3 - outputIndex <- 0 to 1 - } yield ShortChannelId(block, txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) - - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo) - - override type FixtureParam = ActorRef - - override protected def withFixture(test: OneArgTest): Outcome = { - val watcherA = system.actorOf(Props(new FakeWatcher())) - val paramsA = Alice.nodeParams - val routingInfoA = fakeRoutingInfo - routingInfoA.map { - case (a, u1, u2, n1, n2) => - paramsA.db.network.addChannel(a, txid, Satoshi(100000)) - paramsA.db.network.addChannelUpdate(u1) - paramsA.db.network.addChannelUpdate(u2) - paramsA.db.network.addNode(n1) - paramsA.db.network.addNode(n2) - } - val probe = TestProbe() - val switchboard = system.actorOf(Props(new Actor { - override def receive: Receive = { - case msg => probe.ref forward msg - } - }), "switchboard") - - val routerA = system.actorOf(Props(new Router(paramsA, watcherA)), "routerA") - - val sender = TestProbe() - awaitCond({ - sender.send(routerA, 'channels) - val channelsA = sender.expectMsgType[Iterable[ChannelAnnouncement]] - channelsA.size == routingInfoA.size - }, max = 30 seconds) - - test(routerA) - } - - test("prune stale channel") { - router => { - val probe = TestProbe() - probe.ignoreMsg { case TransportHandler.ReadAck(_) => true } - val remoteNodeId = PrivateKey(ByteVector.fromValidHex("01" * 32)).publicKey - - // tell router to ask for our channel ids - probe.send(router, SendChannelQuery(remoteNodeId, probe.ref)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = probe.expectMsgType[QueryChannelRange] - probe.expectMsgType[GossipTimestampFilter] - - // we don't send the first 10 channels, which are stale - val shortChannelIds1 = shortChannelIds.drop(10) - val reply = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1.toByte, ChannelRangeQueries.encodeShortChannelIdsSingle(shortChannelIds1, ChannelRangeQueries.ZLIB_FORMAT, false)) - probe.send(router, PeerRoutingMessage(probe.ref, remoteNodeId, reply)) - - // router should see that it has 10 channels that we don't have, check if they're stale, and prune them - awaitCond({ - probe.send(router, 'channels) - val channels = probe.expectMsgType[Iterable[ChannelAnnouncement]] - val ourIds = channels.map(_.shortChannelId).toSet - ourIds == shortChannelIds1 - }, max = 30 seconds) - } - } -} - -object PruningSpec { - class FakeWatcher extends Actor { - def receive = { - case _: WatchSpentBasic => () - case ValidateRequest(ann) => - val txOut = TxOut(Satoshi(1000000), Script.pay2wsh(Scripts.multiSig2of2(ann.bitcoinKey1, ann.bitcoinKey2))) - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(ann.shortChannelId) - sender ! ValidateResult(ann, Right(Transaction(version = 0, txIn = Nil, txOut = List.fill(outputIndex + 1)(txOut), lockTime = 0), UtxoStatus.Unspent)) - case unexpected => println(s"unexpected : $unexpected") - } - } -} 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 c09fd82df1..e9e99bbda7 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 @@ -16,48 +16,70 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi} 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.{RichWeight, WeightRatios} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} -import org.scalatest.FunSuite +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey} +import org.scalatest.{FunSuite, ParallelTestExecution} import scodec.bits._ +import scala.collection.immutable.SortedMap import scala.util.{Failure, Success} /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ -class RouteCalculationSpec extends FunSuite { +class RouteCalculationSpec extends FunSuite with ParallelTestExecution { import RouteCalculationSpec._ val (a, b, c, d, e, f) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) test("calculate simple route") { + val updates = List( + makeUpdate(1L, a, b, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(2L, b, c, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(3L, c, d, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)) + ).toMap + + val g = makeGraph(updates) + + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + + assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + } + + test("check fee against max pct properly") { + // fee is acceptable is it is either + // - below our maximum fee base + // - below our maximum fraction of the paid amount + + // here we have a maximum fee base of 1 msat, and all our updates have a base fee of 10 msat + // so our fee will always be above the base fee, and we will always check that it is below our maximum percentage + // of the amount being paid val updates = List( - makeUpdate(1L, a, b, 1, 10, cltvDelta = 1), - makeUpdate(2L, b, c, 1, 10, cltvDelta = 1), - makeUpdate(3L, c, d, 1, 10, cltvDelta = 1), - makeUpdate(4L, d, e, 1, 10, cltvDelta = 1) + makeUpdate(1L, a, b, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(2L, b, c, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(3L, c, d, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeUpdate(4L, d, e, 10 msat, 10, cltvDelta = CltvExpiryDelta(1)) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat), currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("calculate the shortest path (correct fees)") { - val (a, b, c, d, e, f) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // a: source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), @@ -75,72 +97,70 @@ class RouteCalculationSpec extends FunSuite { // cost(AE) = 10007 -> A is source, shortest path found // cost(AB) = 10009 - val amountMsat = 10000 - val expectedCost = 10007 + val amount = 10000 msat + val expectedCost = 10007 msat val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 1, feeProportionalMillionth = 200, minHtlcMsat = 0), - makeUpdate(4L, a, e, feeBaseMsat = 1, feeProportionalMillionth = 200, minHtlcMsat = 0), - makeUpdate(2L, b, c, feeBaseMsat = 1, feeProportionalMillionth = 300, minHtlcMsat = 0), - makeUpdate(3L, c, d, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0), - makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0), - makeUpdate(6L, f, d, feeBaseMsat = 1, feeProportionalMillionth = 100, minHtlcMsat = 0) + makeUpdate(1L, a, b, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), + makeUpdate(4L, a, e, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), + makeUpdate(2L, b, c, feeBase = 1 msat, feeProportionalMillionth = 300, minHtlc = 0 msat), + makeUpdate(3L, c, d, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), + makeUpdate(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat), + makeUpdate(6L, f, d, feeBase = 1 msat, feeProportionalMillionth = 100, minHtlc = 0 msat) ).toMap val graph = makeGraph(updates) - val Success(route) = Router.findRoute(graph, a, d, amountMsat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val Success(route) = Router.findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - val totalCost = Graph.pathWeight(hops2Edges(route), amountMsat, false, 0, None).cost + val totalCost = Graph.pathWeight(hops2Edges(route), amount, isPartial = false, 0, None).cost assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil) assert(totalCost === expectedCost) // now channel 5 could route the amount (10000) but not the amount + fees (10007) - val (desc, update) = makeUpdate(5L, e, f, feeBaseMsat = 1, feeProportionalMillionth = 400, minHtlcMsat = 0, maxHtlcMsat = Some(10005L)) + val (desc, update) = makeUpdate(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10005 msat)) val graph1 = graph.addEdge(desc, update) - val Success(route1) = Router.findRoute(graph1, a, d, amountMsat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val Success(route1) = Router.findRoute(graph1, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil) } test("calculate route considering the direct channel pays no fees") { val updates = List( - makeUpdate(1L, a, b, 5, 0), // a -> b - makeUpdate(2L, a, d, 15, 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel - makeUpdate(3L, b, c, 5, 0), // b -> c - makeUpdate(4L, c, d, 5, 0), // c -> d - makeUpdate(5L, d, e, 5, 0) // d -> e + makeUpdate(1L, a, b, 5 msat, 0), // a -> b + makeUpdate(2L, a, d, 15 msat, 0), // a -> d this goes a bit closer to the target and asks for higher fees but is a direct channel + makeUpdate(3L, b, c, 5 msat, 0), // b -> c + makeUpdate(4L, c, d, 5 msat, 0), // c -> d + makeUpdate(5L, d, e, 5 msat, 0) // d -> e ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil)) } test("calculate simple route (add and remove edges") { - val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) - val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route2 = Router.findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route2.map(hops2Ids) === Failure(RouteNotFound)) } test("calculate the shortest path (hardcoded nodes)") { - val (f, g, h, i) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), @@ -149,21 +169,20 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(2L, g, h, 0, 0), - makeUpdate(3L, h, i, 0, 0), - makeUpdate(4L, f, h, 50, 0) // more expensive + makeUpdate(1L, f, g, 0 msat, 0), + makeUpdate(2L, g, h, 0 msat, 0), + makeUpdate(3L, h, i, 0 msat, 0), + makeUpdate(4L, f, h, 50 msat, 0) // more expensive ).toMap val graph = makeGraph(updates) - val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil)) } test("calculate the shortest path (select direct channel)") { - val (f, g, h, i) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), @@ -172,15 +191,15 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(4L, f, i, 50, 0), // our starting node F has a direct channel with I - makeUpdate(2L, g, h, 0, 0), - makeUpdate(3L, h, i, 0, 0) + makeUpdate(1L, f, g, 0 msat, 0), + makeUpdate(4L, f, i, 50 msat, 0), // our starting node F has a direct channel with I + makeUpdate(2L, g, h, 0 msat, 0), + makeUpdate(3L, h, i, 0 msat, 0) ).toMap val graph = makeGraph(updates) - val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(4 :: Nil)) } @@ -189,19 +208,19 @@ class RouteCalculationSpec extends FunSuite { PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // F source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), // G PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), // H - PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target + PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) val updates = List( - makeUpdate(1L, f, g, 1, 0), + makeUpdate(1L, f, g, 1 msat, 0), // the maximum htlc allowed by this channel is only 50msat greater than what we're sending - makeUpdate(2L, g, h, 1, 0, maxHtlcMsat = Some(DEFAULT_AMOUNT_MSAT + 50)), - makeUpdate(3L, h, i, 1, 0) + makeUpdate(2L, g, h, 1 msat, 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), + makeUpdate(3L, h, i, 1 msat, 0) ).toMap val graph = makeGraph(updates) - val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) == Success(1 :: 2 :: 3 :: Nil)) } @@ -210,24 +229,23 @@ class RouteCalculationSpec extends FunSuite { PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // F source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), // G PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), // H - PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target + PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target ) val updates = List( - makeUpdate(1L, f, g, 1, 0), + makeUpdate(1L, f, g, 1 msat, 0), // this channel requires a minimum amount that is larger than what we are sending - makeUpdate(2L, g, h, 1, 0, minHtlcMsat = DEFAULT_AMOUNT_MSAT + 50), - makeUpdate(3L, h, i, 1, 0) + makeUpdate(2L, g, h, 1 msat, 0, minHtlc = DEFAULT_AMOUNT_MSAT + 50.msat), + makeUpdate(3L, h, i, 1 msat, 0) ).toMap val graph = makeGraph(updates) - val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Failure(RouteNotFound)) } test("if there are multiple channels between the same node, select the cheapest") { - val (f, g, h, i) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // F source PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), // G @@ -236,158 +254,150 @@ class RouteCalculationSpec extends FunSuite { ) val updates = List( - makeUpdate(1L, f, g, 0, 0), - makeUpdate(2L, g, h, 5, 5), // expensive g -> h channel - makeUpdate(6L, g, h, 0, 0), // cheap g -> h channel - makeUpdate(3L, h, i, 0, 0) + makeUpdate(1L, f, g, 0 msat, 0), + makeUpdate(2L, g, h, 5 msat, 5), // expensive g -> h channel + makeUpdate(6L, g, h, 0 msat, 0), // cheap g -> h channel + makeUpdate(3L, h, i, 0 msat, 0) ).toMap val graph = makeGraph(updates) - val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil)) } test("calculate longer but cheaper route") { - val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0), - makeUpdate(5L, b, e, 10, 10) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0), + makeUpdate(5L, b, e, 10 msat, 10) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("no local channels") { - val updates = List( - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Failure(RouteNotFound)) } test("route not found") { - val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Failure(RouteNotFound)) } test("route not found (source OR target node not connected)") { - val updates = List( - makeUpdate(2L, b, c, 0, 0), - makeUpdate(4L, c, d, 0, 0) + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(4L, c, d, 0 msat, 0) ).toMap val g = makeGraph(updates).addVertex(a).addVertex(e) - assert(Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) === Failure(RouteNotFound)) - assert(Router.findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) === Failure(RouteNotFound)) + assert(Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)) + assert(Router.findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)) } test("route not found (amount too high OR too low)") { - val highAmount = DEFAULT_AMOUNT_MSAT * 10 val lowAmount = DEFAULT_AMOUNT_MSAT / 10 val updatesHi = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0, maxHtlcMsat = Some(DEFAULT_AMOUNT_MSAT)), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT)), + makeUpdate(3L, c, d, 0 msat, 0) ).toMap val updatesLo = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0, minHtlcMsat = DEFAULT_AMOUNT_MSAT), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0, minHtlc = DEFAULT_AMOUNT_MSAT), + makeUpdate(3L, c, d, 0 msat, 0) ).toMap val g = makeGraph(updatesHi) val g1 = makeGraph(updatesLo) - assert(Router.findRoute(g, a, d, highAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) === Failure(RouteNotFound)) - assert(Router.findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) === Failure(RouteNotFound)) + assert(Router.findRoute(g, a, d, highAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)) + assert(Router.findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)) } test("route to self") { - val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { - val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(1 :: Nil)) } test("directed graph") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap // a->e works, e->a fails val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route2 = Router.findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route2.map(hops2Ids) === Failure(RouteNotFound)) } test("calculate route and return metadata") { - val DUMMY_SIG = Transactions.PlaceHolderSig - val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, 0, 0, 1, 42, 2500, 140, None) - val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, 0, 1, 1, 43, 2501, 141, None) - val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 0, 1, 44, 2502, 142, None) - val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 1, 1, 45, 2503, 143, None) - val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 1, 0, 1, 46, 2504, 144, Some(500000000L)) - val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 0, 1, 1, 47, 2505, 145, None) - val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 0, 1, 48, 2506, 146, None) - val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 1, 1, 49, 2507, 147, None) + val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 0L, 0, 0, CltvExpiryDelta(1), 42 msat, 2500 msat, 140, None) + val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(1L), 1L, 0, 1, CltvExpiryDelta(1), 43 msat, 2501 msat, 141, None) + val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 0, CltvExpiryDelta(1), 44 msat, 2502 msat, 142, None) + val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(2L), 1L, 0, 1, CltvExpiryDelta(1), 45 msat, 2503 msat, 143, None) + val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 1, 0, CltvExpiryDelta(1), 46 msat, 2504 msat, 144, Some(500000000 msat)) + val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(3L), 1L, 0, 1, CltvExpiryDelta(1), 47 msat, 2505 msat, 145, None) + val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 0, CltvExpiryDelta(1), 48 msat, 2506 msat, 146, None) + val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 1, CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) val updates = Map( ChannelDesc(ShortChannelId(1L), a, b) -> uab, @@ -402,105 +412,100 @@ class RouteCalculationSpec extends FunSuite { val g = makeGraph(updates) - val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS).get + val hops = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil) } - test("convert extra hops to channel_update") { + 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), 10, 11, 12) - val extraHop2 = ExtraHop(b, ShortChannelId(2), 20, 21, 22) - val extraHop3 = ExtraHop(c, ShortChannelId(3), 30, 31, 32) - val extraHop4 = ExtraHop(d, ShortChannelId(4), 40, 41, 42) - + 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 fakeUpdates = Router.toFakeUpdates(extraHops, e) - - assert(fakeUpdates == Map( - ChannelDesc(extraHop1.shortChannelId, a, b) -> Router.toFakeUpdate(extraHop1), - ChannelDesc(extraHop2.shortChannelId, b, c) -> Router.toFakeUpdate(extraHop2), - ChannelDesc(extraHop3.shortChannelId, c, d) -> Router.toFakeUpdate(extraHop3), - ChannelDesc(extraHop4.shortChannelId, d, e) -> Router.toFakeUpdate(extraHop4) - )) + val amount = 900 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW + val assistedChannels = Router.toAssistedChannels(extraHops, e, amount.toMilliSatoshi) + assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(extraHop4, e, 1050.sat.toMilliSatoshi)) + assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(extraHop3, d, 1200.sat.toMilliSatoshi)) + assert(assistedChannels(extraHop2.shortChannelId) === AssistedChannel(extraHop2, c, 1400.sat.toMilliSatoshi)) + assert(assistedChannels(extraHop1.shortChannelId) === AssistedChannel(extraHop1, b, 1426.sat.toMilliSatoshi)) } test("blacklist routes") { val updates = List( - makeUpdate(1L, a, b, 0, 0), - makeUpdate(2L, b, c, 0, 0), - makeUpdate(3L, c, d, 0, 0), - makeUpdate(4L, d, e, 0, 0) + makeUpdate(1L, a, b, 0 msat, 0), + makeUpdate(2L, b, c, 0 msat, 0), + makeUpdate(3L, c, d, 0 msat, 0), + makeUpdate(4L, d, e, 0 msat, 0) ).toMap val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Failure(RouteNotFound)) // verify that we left the graph untouched - assert(g.containsEdge(makeUpdate(3L, c, d, 0, 0)._1)) // c -> d + assert(g.containsEdge(makeUpdate(3L, c, d, 0 msat, 0)._1)) // c -> d assert(g.containsVertex(c)) assert(g.containsVertex(d)) // make sure we can find a route if without the blacklist - val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("route to a destination that is not in the graph (with assisted routes)") { val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, d, 10, 10) + makeUpdate(1L, a, b, 10 msat, 10), + makeUpdate(2L, b, c, 10 msat, 10), + makeUpdate(3L, c, d, 10 msat, 10) ).toMap val g = makeGraph(updates) - val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Failure(RouteNotFound)) // now we add the missing edge to reach the destination - val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5, 5) + val (extraDesc, extraUpdate) = makeUpdate(4L, d, e, 5 msat, 5) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } - test("verify that extra hops takes precedence over known channels") { val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, d, 10, 10), - makeUpdate(4L, d, e, 10, 10) + makeUpdate(1L, a, b, 10 msat, 10), + makeUpdate(2L, b, c, 10 msat, 10), + makeUpdate(3L, c, d, 10 msat, 10), + makeUpdate(4L, d, e, 10 msat, 10) ).toMap val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route1.get(1).lastUpdate.feeBaseMsat == 10) + assert(route1.get(1).lastUpdate.feeBaseMsat === 10.msat) - val (extraDesc, extraUpdate) = makeUpdate(2L, b, c, 5, 5) + val (extraDesc, extraUpdate) = makeUpdate(2L, b, c, 5 msat, 5) val extraGraphEdges = Set(GraphEdge(extraDesc, extraUpdate)) - val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS) + val route2 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route2.get(1).lastUpdate.feeBaseMsat == 5) + assert(route2.get(1).lastUpdate.feeBaseMsat === 5.msat) } test("compute ignored channels") { - val f = randomKey.publicKey val g = randomKey.publicKey val h = randomKey.publicKey @@ -516,150 +521,147 @@ class RouteCalculationSpec extends FunSuite { ShortChannelId(6L) -> makeChannel(6L, f, h), ShortChannelId(7L) -> makeChannel(7L, h, i), ShortChannelId(8L) -> makeChannel(8L, i, j) - ) + val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(2L, c, b, 10, 10), - makeUpdate(3L, c, d, 10, 10), - makeUpdate(4L, d, e, 10, 10), - makeUpdate(5L, f, g, 10, 10), - makeUpdate(6L, f, h, 10, 10), - makeUpdate(7L, h, i, 10, 10), - makeUpdate(8L, i, j, 10, 10) + makeUpdate(1L, a, b, 10 msat, 10), + makeUpdate(2L, b, c, 10 msat, 10), + makeUpdate(2L, c, b, 10 msat, 10), + makeUpdate(3L, c, d, 10 msat, 10), + makeUpdate(4L, d, e, 10 msat, 10), + makeUpdate(5L, f, g, 10 msat, 10), + makeUpdate(6L, f, h, 10 msat, 10), + makeUpdate(7L, h, i, 10 msat, 10), + makeUpdate(8L, i, j, 10 msat, 10) ).toMap - val ignored = Router.getIgnoredChannelDesc(updates, ignoreNodes = Set(c, j, randomKey.publicKey)) + val publicChannels = channels.map { case (shortChannelId, announcement) => + val (_, update) = updates.find { case (d, _) => d.shortChannelId == shortChannelId }.get + val (update_1_opt, update_2_opt) = if (Announcements.isNode1(update.channelFlags)) (Some(update), None) else (None, Some(update)) + val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt, update_2_opt) + (shortChannelId, pc) + } + + val ignored = Router.getIgnoredChannelDesc(publicChannels, ignoreNodes = Set(c, j, randomKey.publicKey)) - assert(ignored.toSet === Set( - ChannelDesc(ShortChannelId(2L), b, c), - ChannelDesc(ShortChannelId(2L), c, b), - ChannelDesc(ShortChannelId(3L), c, d), - ChannelDesc(ShortChannelId(8L), i, j) - )) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), b, c))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(2L), c, b))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(3L), c, d))) + assert(ignored.toSet.contains(ChannelDesc(ShortChannelId(8L), i, j))) } test("limit routes to 20 hops") { - val nodes = (for (_ <- 0 until 22) yield randomKey.publicKey).toList - val updates = nodes .zip(nodes.drop(1)) // (0, 1) :: (1, 2) :: ... .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... - .map { case ((na, nb), index) => makeUpdate(index, na, nb, 5, 0) } + .map { case ((na, nb), index) => makeUpdate(index, na, nb, 5 msat, 0) } .toMap val g = makeGraph(updates) - assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS).map(hops2Ids) === Success(0 until 18)) - assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS).map(hops2Ids) === Success(0 until 19)) - assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS).map(hops2Ids) === Success(0 until 20)) - assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS).map(hops2Ids) === Failure(RouteNotFound)) + assert(Router.findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 18)) + assert(Router.findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 19)) + assert(Router.findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 20)) + assert(Router.findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Failure(RouteNotFound)) } test("ignore cheaper route when it has more than 20 hops") { - val nodes = (for (_ <- 0 until 50) yield randomKey.publicKey).toList val updates = nodes .zip(nodes.drop(1)) // (0, 1) :: (1, 2) :: ... .zipWithIndex // ((0, 1), 0) :: ((1, 2), 1) :: ... - .map { case ((na, nb), index) => makeUpdate(index, na, nb, 1, 0) } + .map { case ((na, nb), index) => makeUpdate(index, na, nb, 1 msat, 0) } .toMap - val updates2 = updates + makeUpdate(99, nodes(2), nodes(48), 1000, 0) // expensive shorter route + val updates2 = updates + makeUpdate(99, nodes(2), nodes(48), 1000 msat, 0) // expensive shorter route val g = makeGraph(updates2) - val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route = Router.findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil)) } test("ignore cheaper route when it has more than the requested CLTV") { - val f = randomKey.publicKey val g = makeGraph(List( - makeUpdate(1, a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(2, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(3, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 50), - makeUpdate(4, a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(5, e, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6, f, d, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9) + makeUpdate(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), + makeUpdate(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), + makeUpdate(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(50)), + makeUpdate(4, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(6, f, d, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) ).toMap) - val route = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = 28)) + val route = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(4 :: 5 :: 6 :: Nil)) } test("ignore cheaper route when it grows longer than the requested size") { - val f = randomKey.publicKey val g = makeGraph(List( - makeUpdate(1, a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(2, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(3, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(4, d, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(5, e, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6, b, f, feeBaseMsat = 5, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9) + makeUpdate(1, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(2, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(3, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(4, d, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(5, e, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(6, b, f, feeBase = 5 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)) ).toMap) - val route = Router.findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3)) + val route = Router.findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000) assert(route.map(hops2Ids) === Success(1 :: 6 :: Nil)) } test("ignore loops") { - val updates = List( - makeUpdate(1L, a, b, 10, 10), - makeUpdate(2L, b, c, 10, 10), - makeUpdate(3L, c, a, 10, 10), - makeUpdate(4L, c, d, 10, 10), - makeUpdate(5L, d, e, 10, 10) + makeUpdate(1L, a, b, 10 msat, 10), + makeUpdate(2L, b, c, 10 msat, 10), + makeUpdate(3L, c, a, 10 msat, 10), + makeUpdate(4L, c, d, 10 msat, 10), + makeUpdate(5L, d, e, 10 msat, 10) ).toMap val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil)) } test("ensure the route calculation terminates correctly when selecting 0-fees edges") { - // the graph contains a possible 0-cost path that goes back on its steps ( e -> f, f -> e ) val updates = List( - makeUpdate(1L, a, b, 10, 10), // a -> b - makeUpdate(2L, b, c, 10, 10), - makeUpdate(4L, c, d, 10, 10), - makeUpdate(3L, b, e, 0, 0), // b -> e - makeUpdate(6L, e, f, 0, 0), // e -> f - makeUpdate(6L, f, e, 0, 0), // e <- f - makeUpdate(5L, e, d, 0, 0) // e -> d + makeUpdate(1L, a, b, 10 msat, 10), // a -> b + makeUpdate(2L, b, c, 10 msat, 10), + makeUpdate(4L, c, d, 10 msat, 10), + makeUpdate(3L, b, e, 0 msat, 0), // b -> e + makeUpdate(6L, e, f, 0 msat, 0), // e -> f + makeUpdate(6L, f, e, 0 msat, 0), // e <- f + makeUpdate(5L, e, d, 0 msat, 0) // e -> d ).toMap val g = makeGraph(updates) - val route1 = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS) + val route1 = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(route1.map(hops2Ids) === Success(1 :: 3 :: 5 :: Nil)) } + // @formatter:off /** - * - * +---+ +---+ +---+ - * | A +-----+ | B +----------> | C | - * +-+-+ | +-+-+ +-+-+ - * ^ | ^ | - * | | | | - * | v----> + | | - * +-+-+ <-+-+ +-+-+ - * | D +----------> | E +----------> | F | - * +---+ +---+ +---+ - * - */ + * +---+ +---+ +---+ + * | A +-----+ | B +----------> | C | + * +-+-+ | +-+-+ +-+-+ + * ^ | ^ | + * | | | | + * | v----> + | | + * +-+-+ <-+-+ +-+-+ + * | D +----------> | E +----------> | F | + * +---+ +---+ +---+ + */ + // @formatter:on test("find the k-shortest paths in a graph, k=4") { - val (a, b, c, d, e, f) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b @@ -669,20 +671,19 @@ class RouteCalculationSpec extends FunSuite { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //f ) - val edges = Seq( - makeUpdate(1L, d, a, 1, 0), - makeUpdate(2L, d, e, 1, 0), - makeUpdate(3L, a, e, 1, 0), - makeUpdate(4L, e, b, 1, 0), - makeUpdate(5L, e, f, 1, 0), - makeUpdate(6L, b, c, 1, 0), - makeUpdate(7L, c, f, 1, 0) - ).toMap + makeUpdate(1L, d, a, 1 msat, 0), + makeUpdate(2L, d, e, 1 msat, 0), + makeUpdate(3L, a, e, 1 msat, 0), + makeUpdate(4L, e, b, 1 msat, 0), + makeUpdate(5L, e, f, 1 msat, 0), + makeUpdate(6L, b, c, 1 msat, 0), + makeUpdate(7L, c, f, 1 msat, 0) + ) - val graph = DirectedGraph.makeGraph(edges) + val graph = DirectedGraph().addEdges(edges) - val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) + val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) assert(fourShortestPaths.size === 4) assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F @@ -701,22 +702,21 @@ class RouteCalculationSpec extends FunSuite { PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //h ) - val edges = Seq( - makeUpdate(10L, c, e, 2, 0), - makeUpdate(20L, c, d, 3, 0), - makeUpdate(30L, d, f, 4, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route - makeUpdate(40L, e, d, 1, 0), - makeUpdate(50L, e, f, 2, 0), - makeUpdate(60L, e, g, 3, 0), - makeUpdate(70L, f, g, 2, 0), - makeUpdate(80L, f, h, 1, 0), - makeUpdate(90L, g, h, 2, 0) + makeUpdate(10L, c, e, 2 msat, 0), + makeUpdate(20L, c, d, 3 msat, 0), + makeUpdate(30L, d, f, 4 msat, 5), // D- > F has a higher cost to distinguish it from the 2nd cheapest route + makeUpdate(40L, e, d, 1 msat, 0), + makeUpdate(50L, e, f, 2 msat, 0), + makeUpdate(60L, e, g, 3 msat, 0), + makeUpdate(70L, f, g, 2 msat, 0), + makeUpdate(80L, f, h, 1 msat, 0), + makeUpdate(90L, g, h, 2 msat, 0) ) val graph = DirectedGraph().addEdges(edges) - val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, pathsToFind = 2, None, 0, noopBoundaries) + val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, None, 0, noopBoundaries) assert(twoShortestPaths.size === 2) val shortest = twoShortestPaths(0) @@ -727,30 +727,29 @@ class RouteCalculationSpec extends FunSuite { } 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") { - val f = randomKey.publicKey // simple graph with only 2 possible paths from A to F val edges = Seq( - makeUpdate(1L, a, b, 1, 0), - makeUpdate(1L, b, a, 1, 0), - makeUpdate(2L, b, c, 1, 0), - makeUpdate(2L, c, b, 1, 0), - makeUpdate(3L, c, f, 1, 0), - makeUpdate(3L, f, c, 1, 0), - makeUpdate(4L, c, d, 1, 0), - makeUpdate(4L, d, c, 1, 0), - makeUpdate(41L, d, c, 1, 0), // there is more than one D -> C channel - makeUpdate(5L, d, e, 1, 0), - makeUpdate(5L, e, d, 1, 0), - makeUpdate(6L, e, f, 1, 0), - makeUpdate(6L, f, e, 1, 0) + makeUpdate(1L, a, b, 1 msat, 0), + makeUpdate(1L, b, a, 1 msat, 0), + makeUpdate(2L, b, c, 1 msat, 0), + makeUpdate(2L, c, b, 1 msat, 0), + makeUpdate(3L, c, f, 1 msat, 0), + makeUpdate(3L, f, c, 1 msat, 0), + makeUpdate(4L, c, d, 1 msat, 0), + makeUpdate(4L, d, c, 1 msat, 0), + makeUpdate(41L, d, c, 1 msat, 0), // there is more than one D -> C channel + makeUpdate(5L, d, e, 1 msat, 0), + makeUpdate(5L, e, d, 1 msat, 0), + makeUpdate(6L, e, f, 1 msat, 0), + makeUpdate(6L, f, e, 1 msat, 0) ) val graph = DirectedGraph().addEdges(edges) //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, pathsToFind = 3, None, 0, noopBoundaries) + val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, None, 0, noopBoundaries) assert(foundPaths.size === 2) assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F @@ -758,60 +757,58 @@ class RouteCalculationSpec extends FunSuite { } test("select a random route below the requested fee") { - - val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBaseMsat = 7, maxFeePct = 0) + val strictFeeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 7 msat, maxFeePct = 0) // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 11080003 !! // A -> E -> F -> D has total cost of 10000006 val g = makeGraph(List( - makeUpdate(1L, a, b, feeBaseMsat = 1, 0), - makeUpdate(4L, a, e, feeBaseMsat = 1, 0), - makeUpdate(2L, b, c, feeBaseMsat = 2, 0), - makeUpdate(3L, c, d, feeBaseMsat = 3, 0), - makeUpdate(5L, e, f, feeBaseMsat = 3, 0), - makeUpdate(6L, f, d, feeBaseMsat = 3, 0), - makeUpdate(7L, e, c, feeBaseMsat = 9, 0) + makeUpdate(1L, a, b, feeBase = 1 msat, 0), + makeUpdate(4L, a, e, feeBase = 1 msat, 0), + makeUpdate(2L, b, c, feeBase = 2 msat, 0), + makeUpdate(3L, c, d, feeBase = 3 msat, 0), + makeUpdate(5L, e, f, feeBase = 3 msat, 0), + makeUpdate(6L, f, d, feeBase = 3 msat, 0), + makeUpdate(7L, e, c, feeBase = 9 msat, 0) ).toMap) - (for {_ <- 0 to 10} yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams)).map { - case Failure(thr) => assert(false, thr) + (for {_ <- 0 to 10} yield Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)).map { + case Failure(thr) => fail(thr) case Success(someRoute) => val routeCost = Graph.pathWeight(hops2Edges(someRoute), DEFAULT_AMOUNT_MSAT, isPartial = false, 0, None).cost - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7msat of fees) - assert(routeCost == 5 || routeCost == 6) + assert(routeCost === 5.msat || routeCost === 6.msat) } } test("Use weight ratios to when computing the edge weight") { - - val largeCapacity = 8000000000L + val largeCapacity = 8000000000L msat // A -> B -> C -> D is 'fee optimized', lower fees route (totFees = 2, totCltv = 4000) // A -> E -> F -> D is 'timeout optimized', lower CLTV route (totFees = 3, totCltv = 18) // A -> E -> C -> D is 'capacity optimized', more recent channel/larger capacity route val updates = List( - makeUpdate(1L, a, b, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 13), - makeUpdate(4L, a, e, feeBaseMsat = 0, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdate(2L, b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(3L, c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 500), - makeUpdate(5L, e, f, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(6L, f, d, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 9), - makeUpdate(7L, e, c, feeBaseMsat = 2, 0, minHtlcMsat = 0, maxHtlcMsat = Some(largeCapacity), cltvDelta = 12) + makeUpdate(1L, a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(13)), + makeUpdate(4L, a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(12)), + makeUpdate(2L, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(500)), + makeUpdate(3L, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(500)), + makeUpdate(5L, e, f, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(6L, f, d, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), + makeUpdate(7L, e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = Some(largeCapacity), CltvExpiryDelta(12)) ).toMap val g = makeGraph(updates) - val Success(routeFeeOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS) + val Success(routeFeeOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) assert(hops2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) val Success(routeCltvOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( cltvDeltaFactor = 1, ageFactor = 0, capacityFactor = 0 - )))) + ))), currentBlockHeight = 400000) assert(hops2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) @@ -819,117 +816,122 @@ class RouteCalculationSpec extends FunSuite { cltvDeltaFactor = 0, ageFactor = 0, capacityFactor = 1 - )))) + ))), currentBlockHeight = 400000) assert(hops2Nodes(routeCapacityOptimized) === (a, e) :: (e, c) :: (c, d) :: Nil) } test("prefer going through an older channel if fees and CLTV are the same") { - val currentBlockHeight = 554000 val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), // younger channel - makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x5"), e, f, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x6"), f, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144) + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x1"), a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x4"), a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x2"), b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel + makeUpdateShort(ShortChannelId(s"${currentBlockHeight - 3000}x0x3"), c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x5"), e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"${currentBlockHeight}x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) ).toMap) - Globals.blockCount.set(currentBlockHeight) - val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( ageFactor = 0.33, cltvDeltaFactor = 0.33, capacityFactor = 0.33 - )))) + ))), currentBlockHeight = currentBlockHeight) assert(hops2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) } test("prefer a route with a smaller total CLTV if fees and score are the same") { - val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 10), // smaller CLTV - makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12), - makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 12) + makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), + makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), + makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(10)), // smaller CLTV + makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), + makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)), + makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(12)) ).toMap) - val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( ageFactor = 0.33, cltvDeltaFactor = 0.33, capacityFactor = 0.33 - )))) + ))), currentBlockHeight = 400000) assert(hops2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) } - test("avoid a route that breaks off the max CLTV") { - // A -> B -> C -> D is cheaper but has a total CLTV > 2016! // A -> E -> F -> D is more expensive but has a total CLTV < 2016 val g = makeGraph(List( - makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 1000), - makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBaseMsat = 1, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 900), - makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBaseMsat = 10, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144), - makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBaseMsat = 10, 0, minHtlcMsat = 0, maxHtlcMsat = None, cltvDelta = 144) + makeUpdateShort(ShortChannelId(s"0x0x1"), a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"0x0x4"), a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"0x0x2"), b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(1000)), + makeUpdateShort(ShortChannelId(s"0x0x3"), c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(900)), + makeUpdateShort(ShortChannelId(s"0x0x5"), e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeUpdateShort(ShortChannelId(s"0x0x6"), f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) ).toMap) val Success(routeScoreOptimized) = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( ageFactor = 0.33, cltvDeltaFactor = 0.33, capacityFactor = 0.33 - )))) + ))), currentBlockHeight = 400000) assert(hops2Nodes(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 = List( - ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 0, feeBaseMsat = 1000, 100, Some(15000000000L)), - ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, 14, htlcMinimumMsat = 1, 1000, 10, Some(4294967295L)), - ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 1000, feeBaseMsat = 1000, 100, Some(16777000000L)), - ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 667, 1, Some(16777000000L)), - ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)), - ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)) - ).toMap + val updates = SortedMap( + ShortChannelId("565643x1216x0") -> PublicChannel( + ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), + fundingTxid = ByteVector32.Zeroes, + capacity = 0 sat, + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))) + ), + ShortChannelId("542280x2156x0") -> PublicChannel( + ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), + fundingTxid = ByteVector32.Zeroes, + capacity = 0 sat, + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))) + ), + ShortChannelId("565779x2711x0") -> PublicChannel( + ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), + fundingTxid = ByteVector32.Zeroes, + capacity = 0 sat, + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))) + ) + ) val g = DirectedGraph.makeGraph(updates) - val params = RouteParams(randomize = false, maxFeeBaseMsat = 21000, maxFeePct = 0.03, routeMaxCltv = 1008, routeMaxLength = 6, ratios = Some( + val params = RouteParams(randomize = false, maxFeeBase = 21000 msat, maxFeePct = 0.03, routeMaxCltv = CltvExpiryDelta(1008), routeMaxLength = 6, ratios = Some( WeightRatios(cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5) )) val thisNode = PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96") val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca") - val amount = 351000 + val amount = 351000 msat - Globals.blockCount.set(567634) // simulate mainnet block for heuristic - val Success(route) = Router.findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, params) + val Success(route) = Router.findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic assert(route.size == 2) assert(route.last.nextNodeId == targetNode) } - } object RouteCalculationSpec { val noopBoundaries = { _: RichWeight => true } - val DEFAULT_AMOUNT_MSAT = 10000000 + val DEFAULT_AMOUNT_MSAT = 10000000 msat - val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, maxFeeBaseMsat = 21000, maxFeePct = 0.03, routeMaxCltv = 2016, routeMaxLength = 6, ratios = None) + val DEFAULT_ROUTE_PARAMS = RouteParams(randomize = false, maxFeeBase = 21000 msat, maxFeePct = 0.03, routeMaxCltv = CltvExpiryDelta(2016), routeMaxLength = 6, ratios = None) val DUMMY_SIG = Transactions.PlaceHolderSig @@ -938,29 +940,29 @@ object RouteCalculationSpec { ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, ByteVector.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey) } - def makeUpdate(shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0): (ChannelDesc, ChannelUpdate) = { - makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBaseMsat, feeProportionalMillionth, minHtlcMsat, maxHtlcMsat, cltvDelta) + def makeUpdate(shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0)): (ChannelDesc, ChannelUpdate) = { + makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) } - def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBaseMsat: Int, feeProportionalMillionth: Int, minHtlcMsat: Long = DEFAULT_AMOUNT_MSAT, maxHtlcMsat: Option[Long] = None, cltvDelta: Int = 0): (ChannelDesc, ChannelUpdate) = + 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: Long = 0): (ChannelDesc, ChannelUpdate) = ChannelDesc(shortChannelId, nodeId1, nodeId2) -> ChannelUpdate( signature = DUMMY_SIG, chainHash = Block.RegtestGenesisBlock.hash, shortChannelId = shortChannelId, - timestamp = 0L, - messageFlags = maxHtlcMsat match { + timestamp = timestamp, + messageFlags = maxHtlc match { case Some(_) => 1 case None => 0 }, - channelFlags = 0, + channelFlags = if (Announcements.isNode1(nodeId1, nodeId2)) 0 else 1, cltvExpiryDelta = cltvDelta, - htlcMinimumMsat = minHtlcMsat, - feeBaseMsat = feeBaseMsat, + htlcMinimumMsat = minHtlc, + feeBaseMsat = feeBase, feeProportionalMillionths = feeProportionalMillionth, - htlcMaximumMsat = maxHtlcMsat + htlcMaximumMsat = maxHtlc ) - def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = DirectedGraph.makeGraph(updates) + def makeGraph(updates: Map[ChannelDesc, ChannelUpdate]) = DirectedGraph().addEdges(updates.toSeq) def hops2Ids(route: Seq[Hop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong) 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 58dbbdf1dc..746cbd34d6 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 @@ -18,11 +18,9 @@ package fr.acinq.eclair.router import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Block -import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.{Block, Transaction, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel.BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT import fr.acinq.eclair.crypto.TransportHandler @@ -32,16 +30,15 @@ import fr.acinq.eclair.router.Announcements.makeChannelUpdate import fr.acinq.eclair.router.RouteCalculationSpec.DEFAULT_AMOUNT_MSAT import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.QueryShortChannelIds -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, randomKey} import scodec.bits._ -import scala.collection.SortedSet import scala.compat.Platform import scala.concurrent.duration._ /** - * Created by PM on 29/08/2016. - */ + * Created by PM on 29/08/2016. + */ class RouterSpec extends BaseRouterSpec { @@ -54,21 +51,21 @@ class RouterSpec extends BaseRouterSpec { val channelId_ac = ShortChannelId(420000, 5, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) - val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId_ac, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat) // a-x will not be found val priv_x = randomKey val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey) - val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ax = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_x.publicKey, chan_ax.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat) // a-y will have an invalid script val priv_y = randomKey val priv_funding_y = randomKey val chan_ay = channelAnnouncement(ShortChannelId(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, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat) // a-z will be spent 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 update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L) + val update_az = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_z.publicKey, chan_az.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat) router ! PeerRoutingMessage(null, remoteNodeId, chan_ac) router ! PeerRoutingMessage(null, remoteNodeId, chan_ax) @@ -186,9 +183,9 @@ class RouterSpec extends BaseRouterSpec { val x = PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73") val y = PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a") val z = PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484") - val extraHop_cx = ExtraHop(c, ShortChannelId(1), 10, 11, 12) - val extraHop_xy = ExtraHop(x, ShortChannelId(2), 10, 11, 12) - val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20, 21, 22) + 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, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil))) val res = sender.expectMsgType[RouteResponse] assert(res.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil) @@ -203,7 +200,7 @@ class RouterSpec extends BaseRouterSpec { assert(res.hops.map(_.nodeId).toList === a :: b :: c :: Nil) assert(res.hops.last.nextNodeId === d) - val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, cltvExpiryDelta = 3, 0, feeBaseMsat = 153000, feeProportionalMillionths = 4, htlcMaximumMsat = 500000000L, enable = false) + val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, 500000000L msat, enable = false) sender.send(router, PeerRoutingMessage(null, remoteNodeId, channelUpdate_cd1)) sender.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams)) @@ -236,7 +233,24 @@ class RouterSpec extends BaseRouterSpec { val state = sender.expectMsgType[RoutingState] assert(state.channels.size == 4) assert(state.nodes.size == 6) - assert(state.updates.size == 8) + assert(state.channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 8) + } + + ignore("send network statistics") { fixture => + import fixture._ + val sender = TestProbe() + sender.send(router, GetNetworkStats) + assert(sender.expectMsgType[Option[NetworkStats]] === None) + + // Network statistics should be computed after initial sync + router ! SyncProgress(1.0) + sender.send(router, GetNetworkStats) + + val Some(stats) = sender.expectMsgType[Option[NetworkStats]] + assert(stats.channels === 4) + assert(stats.nodes === 6) + assert(stats.capacity.median === 1000000.sat) + assert(stats.cltvExpiryDelta.median === CltvExpiryDelta(6)) } test("given a pre-computed route add the proper channel updates") { fixture => @@ -251,35 +265,35 @@ class RouterSpec extends BaseRouterSpec { assert(response.hops.map(_.nodeId).toList == preComputedRoute.dropRight(1).toList) assert(response.hops.last.nextNodeId == preComputedRoute.last) // On Android we strip the signatures and chain hash - assert(response.hops.map(_.lastUpdate).toList == List(channelUpdate_ab.copy(signature = null, chainHash = null), channelUpdate_bc.copy(signature = null, chainHash = null), channelUpdate_cd.copy(signature = null, chainHash = null))) + assert(response.hops.map(_.lastUpdate).toList == List(channelUpdate_ab.copy(signature = null), channelUpdate_bc.copy(signature = null), channelUpdate_cd.copy(signature = null))) } ignore("ask for channels that we marked as stale for which we receive a new update") { fixture => import fixture._ - val blockHeight = Globals.blockCount.get().toInt - 2020 + val blockHeight = 400000 - 2020 val channelId = ShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) val timestamp = (Platform.currentTime.milliseconds - 14.days - 1.day).toSeconds - val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 5, timestamp = timestamp) + val update = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = timestamp) val probe = TestProbe() probe.ignoreMsg { case _: TransportHandler.ReadAck => true } probe.send(router, PeerRoutingMessage(null, remoteNodeId, announcement)) watcher.expectMsgType[ValidateRequest] probe.send(router, PeerRoutingMessage(null, remoteNodeId, update)) - watcher.send(router, ValidateResult(announcement, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) + watcher.send(router, ValidateResult(announcement, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) probe.send(router, TickPruneStaleChannels) val sender = TestProbe() sender.send(router, GetRoutingState) - val state = sender.expectMsgType[RoutingState] + sender.expectMsgType[RoutingState] - - val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, cltvExpiryDelta = 7, htlcMinimumMsat = 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, htlcMaximumMsat = 500000000L, timestamp = Platform.currentTime.millisecond.toSeconds) + val update1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 500000000L msat, timestamp = Platform.currentTime.millisecond.toSeconds) // we want to make sure that transport receives the query val transport = TestProbe() probe.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, update1)) val query = transport.expectMsgType[QueryShortChannelIds] - assert(ChannelRangeQueries.decodeShortChannelIds(query.data)._2 == SortedSet(channelId)) + assert(query.shortChannelIds.array == List(channelId)) } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala deleted file mode 100644 index a017241d77..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncExSpec.scala +++ /dev/null @@ -1,83 +0,0 @@ -package fr.acinq.eclair.router - -import akka.actor.ActorSystem -import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.eclair._ -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike - -import scala.collection.immutable.TreeMap -import scala.concurrent.duration._ - - -class RoutingSyncExSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { - import RoutingSyncSpec.makeFakeRoutingInfo - - test("handle chanel range extended queries") { - val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) - val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId - - // ask router to send a channel range query - sender.send(router, SendChannelQueryEx(remoteNodeId, sender.ref)) - val QueryChannelRangeEx(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRangeEx] - sender.expectMsgType[GossipTimestampFilter] - - - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap - val initChannels = fakeRoutingInfo.values.map(_._1).foldLeft(TreeMap.empty[ShortChannelId, ChannelAnnouncement]) { case (m, c) => m + (c.shortChannelId -> c) } - val initChannelUpdates = fakeRoutingInfo.values.flatMap(t => Seq(t._2, t._3)).map { u => - val desc = Router.getDesc(u, initChannels(u.shortChannelId)) - (desc) -> u - }.toMap - - // split our anwser in 3 blocks - val List(block1) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) - val List(block2) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) - val List(block3) = ChannelRangeQueriesEx.encodeShortChannelIdAndTimestamps(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), Router.getTimestamp(initChannels, initChannelUpdates), ChannelRangeQueriesEx.UNCOMPRESSED_FORMAT) - - // send first block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIdAndTimestamps))) - // router should ask for our first block of ids - val QueryShortChannelIdsEx(_, _, data1) = sender.expectMsgType[QueryShortChannelIdsEx] - val (_, shortChannelIds1, false) = ChannelRangeQueries.decodeShortChannelIds(data1) - assert(shortChannelIds1 == shortChannelIds.take(100)) - - // send second block - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyChannelRangeEx(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIdAndTimestamps))) - - // router should not ask for more ids, it already has a pending query ! - sender.expectNoMsg(1 second) - - // send the first 50 items - shortChannelIds1.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // send the last 50 items - shortChannelIds1.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, cu2)) - }) - sender.expectNoMsg(1 second) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(sender.ref, remoteNodeId, ReplyShortChannelIdsEndEx(chainHash, 1.toByte))) - - // router should ask for our second block of ids - val QueryShortChannelIdsEx(_, _, data2) = sender.expectMsgType[QueryShortChannelIdsEx] - val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) - assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) - } -} 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 1c4d851b65..9bcd2f792f 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 @@ -16,82 +16,232 @@ package fr.acinq.eclair.router -import akka.actor.ActorSystem +import akka.actor.{Actor, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ -import org.scalatest.FunSuiteLike +import org.scalatest.{FunSuiteLike, Ignore, ParallelTestExecution} +import scodec.bits.HexStringSyntax +import scala.collection.immutable.TreeMap +import scala.collection.{SortedSet, immutable, mutable} +import scala.compat.Platform import scala.concurrent.duration._ +//TODO: re-enable this using a modified version of the old test +// as is it won't work on because on Android router just ignore querier +@Ignore +class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with ParallelTestExecution { -class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { + import RoutingSyncSpec._ - import RoutingSyncSpec.makeFakeRoutingInfo + val fakeRoutingInfo: TreeMap[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec + .shortChannelIds + .take(60) + .foldLeft(TreeMap.empty[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { + case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(shortChannelId)) + } - val shortChannelIds = ChannelRangeQueriesSpec.shortChannelIds.take(350) - val fakeRoutingInfo = shortChannelIds.map(makeFakeRoutingInfo).map(t => t._1.shortChannelId -> t).toMap + class YesWatcher extends Actor { + override def receive: Receive = { + case ValidateRequest(c) => + val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(c.bitcoinKey1, c.bitcoinKey2))) + val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) + val fakeFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format + lockTime = 0) + sender ! ValidateResult(c, Right(fakeFundingTx, UtxoStatus.Unspent)) + } + } - test("handle channel range queries") { - val params = TestConstants.Alice.nodeParams - val router = TestFSMRef(new Router(params, TestProbe().ref)) - val transport = TestProbe() + case class BasicSyncResult(ranges: Int, queries: Int, channels: Int, updates: Int, nodes: Int) + + case class SyncResult(ranges: Seq[ReplyChannelRange], queries: Seq[QueryShortChannelIds], channels: Seq[ChannelAnnouncement], updates: Seq[ChannelUpdate], nodes: Seq[NodeAnnouncement]) { + def counts = BasicSyncResult(ranges.size, queries.size, channels.size, updates.size, nodes.size) + } + + def sync(src: TestFSMRef[State, Data, Router], tgt: TestFSMRef[State, Data, Router], extendedQueryFlags_opt: Option[QueryChannelRangeTlv]): SyncResult = { val sender = TestProbe() - sender.ignoreMsg { case _: TransportHandler.ReadAck => true } - val remoteNodeId = TestConstants.Bob.nodeParams.nodeId + val pipe = TestProbe() + pipe.ignoreMsg { + case _: TransportHandler.ReadAck => true + case _: GossipTimestampFilter => true + } + val srcId = src.underlyingActor.nodeParams.nodeId + val tgtId = tgt.underlyingActor.nodeParams.nodeId + sender.send(src, SendChannelQuery(tgtId, pipe.ref, extendedQueryFlags_opt)) + // src sends a query_channel_range to bob + val qcr = pipe.expectMsgType[QueryChannelRange] + pipe.send(tgt, PeerRoutingMessage(pipe.ref, srcId, qcr)) + // this allows us to know when the last reply_channel_range has been set + pipe.send(tgt, 'data) + // tgt answers with reply_channel_ranges + val rcrs = pipe.receiveWhile() { + case rcr: ReplyChannelRange => rcr + } + pipe.expectMsgType[Data] + rcrs.foreach(rcr => pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, rcr))) + // then src will now query announcements + var queries = Vector.empty[QueryShortChannelIds] + var channels = Vector.empty[ChannelAnnouncement] + var updates = Vector.empty[ChannelUpdate] + var nodes = Vector.empty[NodeAnnouncement] + while (src.stateData.sync.nonEmpty) { + // for each chunk, src sends a query_short_channel_id + val query = pipe.expectMsgType[QueryShortChannelIds] + pipe.send(tgt, PeerRoutingMessage(pipe.ref, srcId, query)) + queries = queries :+ query + val announcements = pipe.receiveWhile() { + case c: ChannelAnnouncement => + channels = channels :+ c + c + case u: ChannelUpdate => + updates = updates :+ u + u + case n: NodeAnnouncement => + nodes = nodes :+ n + n + } + // tgt replies with announcements + announcements.foreach(ann => pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, ann))) + // and tgt ends this chunk with a reply_short_channel_id_end + val rscie = pipe.expectMsgType[ReplyShortChannelIdsEnd] + pipe.send(src, PeerRoutingMessage(pipe.ref, tgtId, rscie)) + } + SyncResult(rcrs, queries, channels, updates, nodes) + } - // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] - sender.expectMsgType[GossipTimestampFilter] + def countUpdates(channels: Map[ShortChannelId, PublicChannel]) = channels.values.foldLeft(0) { + case (count, pc) => count + pc.update_1_opt.map(_ => 1).getOrElse(0) + pc.update_2_opt.map(_ => 1).getOrElse(0) + } - // split our answer in 3 blocks - val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block2) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(100).take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) - val List(block3) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.drop(200).take(150), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + test("sync with standard channel queries") { + val watcher = system.actorOf(Props(new YesWatcher())) + val alice = TestFSMRef(new Router(Alice.nodeParams, watcher)) + val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) + val charlieId = randomKey.publicKey + val sender = TestProbe() + val extendedQueryFlags_opt = None - // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) - // router should ask for our first block of ids - val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds1, false) = ChannelRangeQueries.decodeShortChannelIds(data1) - assert(shortChannelIds1 == shortChannelIds.take(100)) - - // send second block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block2.firstBlock, block2.numBlocks, 1, block2.shortChannelIds))) - - // send the first 50 items - shortChannelIds1.take(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - - // send the last 50 items - shortChannelIds1.drop(50).foreach(id => { - val (ca, cu1, cu2, _, _) = fakeRoutingInfo(id) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ca)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu1)) - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, cu2)) - }) - - // during that time, router should not have asked for more ids, it already has a pending query ! - transport.expectNoMsg(200 millis) - - // now send our ReplyShortChannelIdsEnd message - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyShortChannelIdsEnd(chainHash, 1.toByte))) - - // router should ask for our second block of ids - val QueryShortChannelIds(_, data2) = transport.expectMsgType[QueryShortChannelIds] - val (_, shortChannelIds2, false) = ChannelRangeQueries.decodeShortChannelIds(data2) - assert(shortChannelIds2 == shortChannelIds.drop(100).take(100)) + // 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) + + // add some channels and updates to bob and resync + fakeRoutingInfo.take(10).values.foreach { + case (pc, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.ann)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_1_opt.get)) + // we don't send channel_update #2 + 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) + + // 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) + + // add everything (duplicates will be ignored) + fakeRoutingInfo.values.foreach { + case (pc, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.ann)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_1_opt.get)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) + 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 = 12, 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 = { + val watcher = system.actorOf(Props(new YesWatcher())) + val alice = TestFSMRef(new Router(Alice.nodeParams.copy(routerConf = Alice.nodeParams.routerConf.copy(requestNodeAnnouncements = requestNodeAnnouncements)), watcher)) + val bob = TestFSMRef(new Router(Bob.nodeParams, watcher)) + val charlieId = randomKey.publicKey + val sender = TestProbe() + 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) + + // add some channels and updates to bob and resync + fakeRoutingInfo.take(10).values.foreach { + case (pc, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.ann)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_1_opt.get)) + // we don't send channel_update #2 + 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) + + // 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) + + // add everything (duplicates will be ignored) + fakeRoutingInfo.values.foreach { + case (pc, na1, na2) => + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.ann)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_1_opt.get)) + sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) + 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 = 10, 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) = { + val PublicChannel(c, _, _, Some(u1), Some(u2)) = fakeRoutingInfo.values.toList(shortChannelId)._1 + makeNewerChannelUpdate(c, if (side) u1 else u2) + } + + val bumpedUpdates = (List(0, 3, 7).map(touchUpdate(_, true)) ++ List(1, 3, 9).map(touchUpdate(_, 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) + } + + test("sync with extended channel queries (don't request node announcements)") { + syncWithExtendedQueries(false) + } + + test("sync with extended channel queries (request node announcements)") { + syncWithExtendedQueries(true) } test("reset sync state on reconnection") { @@ -103,39 +253,93 @@ class RoutingSyncSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val remoteNodeId = TestConstants.Bob.nodeParams.nodeId // ask router to send a channel range query - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) - val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks) = sender.expectMsgType[QueryChannelRange] + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None)) + val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - val List(block1) = ChannelRangeQueries.encodeShortChannelIds(firstBlockNum, numberOfBlocks, shortChannelIds.take(100), ChannelRangeQueries.UNCOMPRESSED_FORMAT) + val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, fakeRoutingInfo.take(params.routerConf.channelQueryChunkSize).keys.toList), None, None) // send first block - sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, ReplyChannelRange(chainHash, block1.firstBlock, block1.numBlocks, 1, block1.shortChannelIds))) + sender.send(router, PeerRoutingMessage(transport.ref, remoteNodeId, block1)) // router should ask for our first block of ids - val QueryShortChannelIds(_, data1) = transport.expectMsgType[QueryShortChannelIds] - // router should think that it is mssing 100 channels + assert(transport.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.totalMissingCount == 100) + assert(sync.total == 1) // simulate a re-connection - sender.send(router, SendChannelQuery(remoteNodeId, sender.ref)) + sender.send(router, SendChannelQuery(remoteNodeId, sender.ref, None)) sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] assert(router.stateData.sync.get(remoteNodeId).isEmpty) } -} + test("sync progress") { + + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), TlvStream.empty) + + val nodeidA = randomKey.publicKey + val nodeidB = randomKey.publicKey + + val (sync1, _) = Router.addToSync(Map.empty, nodeidA, List(req, req, req, req)) + assert(Router.syncProgress(sync1) == SyncProgress(0.25D)) + + val (sync2, _) = Router.addToSync(sync1, nodeidB, List(req, req, req, req, req, req, req, req, req, req, req, req)) + assert(Router.syncProgress(sync2) == SyncProgress(0.125D)) + + // let's assume we made some progress + val sync3 = sync2 + .updated(nodeidA, sync2(nodeidA).copy(pending = List(req))) + .updated(nodeidB, sync2(nodeidB).copy(pending = List(req))) + assert(Router.syncProgress(sync3) == SyncProgress(0.875D)) + } +} object RoutingSyncSpec { - def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (ChannelAnnouncement, ChannelUpdate, ChannelUpdate, NodeAnnouncement, NodeAnnouncement) = { - val (priv_a, priv_b, priv_funding_a, priv_funding_b) = (randomKey, randomKey, randomKey, randomKey) - val channelAnn_ab = channelAnnouncement(shortChannelId, priv_a, priv_b, priv_funding_a, priv_funding_b) - val TxCoordinates(blockHeight, _, _) = ShortChannelId.coordinates(shortChannelId) - val channelUpdate_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_b.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = blockHeight) - val channelUpdate_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, priv_a.publicKey, shortChannelId, cltvExpiryDelta = 7, 0, feeBaseMsat = 766000, feeProportionalMillionths = 10, 500000000L, timestamp = blockHeight) - val nodeAnnouncement_a = makeNodeAnnouncement(priv_a, "a", Color(0, 0, 0), List()) - val nodeAnnouncement_b = makeNodeAnnouncement(priv_b, "b", Color(0, 0, 0), List()) - (channelAnn_ab, channelUpdate_ab, channelUpdate_ba, nodeAnnouncement_a, nodeAnnouncement_b) + + lazy val shortChannelIds: immutable.SortedSet[ShortChannelId] = (for { + block <- 400000 to 420000 + txindex <- 0 to 5 + outputIndex <- 0 to 1 + } yield ShortChannelId(block, txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + + // 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 unused = randomKey + + def makeFakeRoutingInfo(shortChannelId: ShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { + val timestamp = Platform.currentTime / 1000 + val (priv1, priv2) = { + val (priv_a, priv_b) = (randomKey, randomKey) + if (Announcements.isNode1(priv_a.publicKey, priv_b.publicKey)) (priv_a, priv_b) else (priv_b, priv_a) + } + val priv_funding1 = unused + val priv_funding2 = unused + pub2priv += (priv1.publicKey -> priv1) + pub2priv += (priv2.publicKey -> priv2) + 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(), hex"0200") + val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), hex"00") + val publicChannel = PublicChannel(channelAnn_12, ByteVector32.Zeroes, Satoshi(0), Some(channelUpdate_12), Some(channelUpdate_21)) + (publicChannel, nodeAnnouncement_1, nodeAnnouncement_2) + } + + def makeNewerChannelUpdate(channelAnnouncement: ChannelAnnouncement, channelUpdate: ChannelUpdate): ChannelUpdate = { + val (local, remote) = if (Announcements.isNode1(channelUpdate.channelFlags)) (channelAnnouncement.nodeId1, channelAnnouncement.nodeId2) else (channelAnnouncement.nodeId2, channelAnnouncement.nodeId1) + val priv = pub2priv(local) + makeChannelUpdate(channelUpdate.chainHash, priv, remote, channelUpdate.shortChannelId, + channelUpdate.cltvExpiryDelta, channelUpdate.htlcMinimumMsat, + channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, + channelUpdate.htlcMinimumMsat, Announcements.isEnabled(channelUpdate.channelFlags), channelUpdate.timestamp + 5000) } + + def makeFakeNodeAnnouncement(nodeId: PublicKey): NodeAnnouncement = { + val priv = pub2priv(nodeId) + makeNodeAnnouncement(priv, "", Color(0, 0, 0), List(), hex"00") + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala index 37aa3d161e..942f48b49b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimReceivedHtlcSpec.scala @@ -79,21 +79,21 @@ class ClaimReceivedHtlcSpec extends FunSuite { val tx = Transaction( version = 2, txIn = TxIn(OutPoint(ByteVector32.Zeroes, 0), ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(10 satoshi, Script.pay2wsh(htlcScript)) :: Nil, + txOut = TxOut(10 sat, Script.pay2wsh(htlcScript)) :: Nil, lockTime = 0) // this tx tries to spend the previous tx val tx1 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, 0xffffffff) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = 0) test("Alice can spend this HTLC after a delay if she knows the payment hash") { val tx2 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, reltimeout + 1) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = abstimeout + 1) val sig = Transaction.signInput(tx2, 0, Script.write(htlcScript), SIGHASH_ALL, tx.txOut(0).amount, 1, Alice.finalKey) @@ -107,7 +107,7 @@ class ClaimReceivedHtlcSpec extends FunSuite { val tx2 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, reltimeout + 1) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Bob.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Bob.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = abstimeout + 1) val sig = Transaction.signInput(tx2, 0, Script.write(htlcScript), SIGHASH_ALL, tx.txOut(0).amount, 1, Bob.finalKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala index 824aa1cc57..8cab7fe94b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/ClaimSentHtlcSpec.scala @@ -77,21 +77,21 @@ class ClaimSentHtlcSpec extends FunSuite { val tx = Transaction( version = 2, txIn = TxIn(OutPoint(ByteVector32.Zeroes, 0), ByteVector.empty, 0xffffffffL) :: Nil, - txOut = TxOut(10 satoshi, Script.pay2wsh(htlcScript)) :: Nil, + txOut = TxOut(10 sat, Script.pay2wsh(htlcScript)) :: Nil, lockTime = 0) // this tx tries to spend the previous tx val tx1 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, 0xffffffff) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = 0) test("Alice can spend this HTLC after a delay") { val tx2 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, sequence = reltimeout + 1) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = abstimeout + 1) val sig = Transaction.signInput(tx2, 0, redeemScript, SIGHASH_ALL, tx.txOut(0).amount, 1, Alice.finalKey) @@ -105,7 +105,7 @@ class ClaimSentHtlcSpec extends FunSuite { val tx2 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, sequence = reltimeout + 1) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = abstimeout - 1) val sig = Transaction.signInput(tx2, 0, redeemScript, SIGHASH_ALL, tx.txOut(0).amount, 1, Alice.finalKey) @@ -122,7 +122,7 @@ class ClaimSentHtlcSpec extends FunSuite { val tx2 = Transaction( version = 2, txIn = TxIn(OutPoint(tx, 0), ByteVector.empty, sequence = reltimeout - 1) :: Nil, - txOut = TxOut(10 satoshi, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, + txOut = TxOut(10 sat, OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(Alice.finalPubKey.value)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) :: Nil, lockTime = abstimeout + 1) val sig = Transaction.signInput(tx2, 0, redeemScript, SIGHASH_ALL, tx.txOut(0).amount, 1, Alice.finalKey) 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 1b95b50856..b0a3a3953d 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 @@ -17,53 +17,52 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto} -import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, randomBytes32} import org.scalatest.FunSuite - class CommitmentSpecSpec extends FunSuite { test("add, fulfill and fail htlcs from the sender side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocalMsat = 5000 * 1000, toRemoteMsat = 0) + val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocal = 5000000 msat, toRemote = 0 msat) val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) + 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(DirectedHtlc(OUT, add1)), toLocalMsat = 3000 * 1000)) + assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocal = 3000000 msat)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) + 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(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocalMsat = 2000 * 1000)) + assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, 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(DirectedHtlc(OUT, add2)), toRemoteMsat = 2000 * 1000)) + assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(OUT, 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(), toLocalMsat = 3000 * 1000)) + assert(spec4 === spec3.copy(htlcs = Set(), toLocal = 3000000 msat)) } test("add, fulfill and fail htlcs from the receiver side") { - val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocalMsat = 0, toRemoteMsat = 5000 * 1000) + val spec = CommitmentSpec(htlcs = Set(), feeratePerKw = 1000, toLocal = 0 msat, toRemote = (5000 * 1000) msat) val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) + 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(DirectedHtlc(IN, add1)), toRemoteMsat = 3000 * 1000)) + assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemote = (3000 * 1000 msat))) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) + 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(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemoteMsat = 2000 * 1000)) + assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, 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(DirectedHtlc(IN, add2)), toLocalMsat = 2000 * 1000)) + assert(spec3 === spec2.copy(htlcs = Set(DirectedHtlc(IN, 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(), toRemoteMsat = 3000 * 1000)) + assert(spec4 === spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) } } 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 013d70b8fb..a7f88649a9 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 @@ -17,12 +17,12 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin._ -import fr.acinq.eclair.TestConstants +import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} import fr.acinq.eclair.wire.UpdateAddHtlc +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, TestConstants} import grizzled.slf4j.Logging import org.scalatest.FunSuite import scodec.bits._ @@ -65,8 +65,8 @@ class TestVectorsSpec extends FunSuite with Logging { */ object Local { val commitTxNumber = 42 - val toSelfDelay = 144 - val dustLimit = Satoshi(546) + val toSelfDelay = CltvExpiryDelta(144) + val dustLimit = 546 sat val payment_basepoint_secret = PrivateKey(hex"1111111111111111111111111111111111111111111111111111111111111111") val payment_basepoint = payment_basepoint_secret.publicKey val revocation_basepoint_secret = PrivateKey(hex"2222222222222222222222222222222222222222222222222222222222222222") @@ -107,8 +107,8 @@ class TestVectorsSpec extends FunSuite with Logging { object Remote { val commitTxNumber = 42 - val toSelfDelay = 144 - val dustLimit = Satoshi(546) + val toSelfDelay = CltvExpiryDelta(144) + val dustLimit = 546 sat val payment_basepoint_secret = PrivateKey(hex"4444444444444444444444444444444444444444444444444444444444444444") val payment_basepoint = payment_basepoint_secret.publicKey val revocation_basepoint_secret = PrivateKey(hex"2222222222222222222222222222222222222222222222222222222222222222") @@ -154,11 +154,11 @@ class TestVectorsSpec extends FunSuite with Logging { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) ) val htlcScripts = htlcs.map(htlc => htlc.direction match { case OUT => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash)) @@ -178,8 +178,8 @@ class TestVectorsSpec extends FunSuite with Logging { } def run(spec: CommitmentSpec) = { - logger.info(s"to_local_msat: ${spec.toLocalMsat}") - logger.info(s"to_remote_msat: ${spec.toRemoteMsat}") + logger.info(s"to_local_msat: ${spec.toLocal}") + logger.info(s"to_remote_msat: ${spec.toRemote}") logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") val commitTx = { @@ -234,7 +234,7 @@ class TestVectorsSpec extends FunSuite with Logging { val (unsignedHtlcTimeoutTxs, unsignedHtlcSuccessTxs) = Transactions.makeHtlcTxs( commitTx.tx, - Satoshi(Local.dustLimit.toLong), + Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, Local.delayed_payment_privkey.publicKey, Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key @@ -286,9 +286,9 @@ class TestVectorsSpec extends FunSuite with Logging { test("simple commitment tx with no HTLCs") { val name = "simple commitment tx with no HTLCs" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = 15000, toLocalMsat = 7000000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = Set.empty, feeratePerKw = 15000, toLocal = 7000000000L msat, toRemote = 3000000000L msat) - val (commitTx, htlcTxs) = run(spec) + val (commitTx, _) = run(spec) assert(commitTx.tx.txOut.length == 2) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) @@ -297,9 +297,9 @@ class TestVectorsSpec extends FunSuite with Logging { test("commitment tx with all 5 htlcs untrimmed (minimum feerate)") { val name = "commitment tx with all 5 htlcs untrimmed (minimum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocal = 6988000000L msat, toRemote = 3000000000L msat) - val (commitTx, htlcTxs) = run(spec) + val (commitTx, _) = run(spec) assert(commitTx.tx.txOut.length == 7) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) } @@ -308,13 +308,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 7 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 7) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -322,13 +322,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 6 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 6) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -336,13 +336,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 6 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 6) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -350,13 +350,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 5 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 5) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -364,13 +364,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 5 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 5) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -378,13 +378,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 4 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 4) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -392,13 +392,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 4 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 4) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -406,13 +406,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 3 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 3) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -420,13 +420,13 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 3 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 3) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } @@ -434,52 +434,52 @@ class TestVectorsSpec extends FunSuite with Logging { val name = "commitment tx with 2 outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 2) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } test("commitment tx with 2 outputs untrimmed (maximum feerate)") { val name = "commitment tx with 2 outputs untrimmed (maximum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 2) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } test("commitment tx with 1 output untrimmed (minimum feerate)") { val name = "commitment tx with 1 output untrimmed (minimum feerate)" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 1) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } test("commitment tx with fee greater than funder amount") { val name = "commitment tx with fee greater than funder amount" logger.info(s"name: $name") - val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651936, toLocalMsat = 6988000000L, toRemoteMsat = 3000000000L) + val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651936, toLocal = 6988000000L msat, toRemote = 3000000000L msat) val (commitTx, htlcTxs) = run(spec) assert(commitTx.tx.txOut.length == 1) assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) - val check = (0 to 4).map(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).flatten.toSet.map { s: String => Transaction.read(s) } + val check = (0 to 4).flatMap(i => results(name).get(s"output htlc_success_tx $i").toSeq ++ results(name).get(s"output htlc_timeout_tx $i").toSeq).toSet.map { s: String => Transaction.read(s) } assert(htlcTxs.map(_.tx).toSet == check) } } 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 f3819c8bc8..d88099179b 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 @@ -20,12 +20,12 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, Protocol, Satoshi, Script, Transaction, TxOut, millibtc2satoshi} import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc +import fr.acinq.eclair.{MilliSatoshi, TestConstants, randomBytes32, _} import grizzled.slf4j.Logging import org.scalatest.FunSuite @@ -33,8 +33,8 @@ import scala.io.Source import scala.util.{Failure, Random, Success, Try} /** - * Created by PM on 16/12/2016. - */ + * Created by PM on 16/12/2016. + */ class TransactionsSpec extends FunSuite with Logging { @@ -63,14 +63,14 @@ class TransactionsSpec extends FunSuite with Logging { test("compute fees") { // see BOLT #3 specs val htlcs = Set( - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, TestConstants.emptyOnionPacket)) + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 5000000 msat, ByteVector32.Zeroes, CltvExpiry(552), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, ByteVector32.Zeroes, CltvExpiry(553), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 7000000 msat, ByteVector32.Zeroes, CltvExpiry(550), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 800000 msat, ByteVector32.Zeroes, CltvExpiry(551), TestConstants.emptyOnionPacket)) ) - val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocalMsat = 0, toRemoteMsat = 0) - val fee = Transactions.commitTxFee(Satoshi(546), spec) - assert(fee == Satoshi(5340)) + val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocal = 0 msat, toRemote = 0 msat) + val fee = Transactions.commitTxFee(546 sat, spec) + assert(fee === 5340.sat) } test("check pre-computed transaction weights") { @@ -81,15 +81,16 @@ class TransactionsSpec extends FunSuite with Logging { val remoteHtlcPriv = PrivateKey(randomBytes32) val localFinalPriv = PrivateKey(randomBytes32) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)) - val localDustLimit = Satoshi(546) - val toLocalDelay = 144 + val localDustLimit = 546 sat + val toLocalDelay = CltvExpiryDelta(144) val feeratePerKw = fr.acinq.eclair.MinimumFeeratePerKw + val blockHeight = 400000 { // ClaimP2WPKHOutputTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimP2WPKHOutputTx val pubKeyScript = write(pay2wpkh(localPaymentPriv.publicKey)) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val claimP2WPKHOutputTx = makeClaimP2WPKHOutputTx(commitTx, localDustLimit, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimP2WPKHOutputTx, localPaymentPriv.publicKey, PlaceHolderSig).tx) @@ -101,7 +102,7 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcDelayedTx // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the ClaimDelayedOutputTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) + val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val claimHtlcDelayedTx = makeClaimDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcDelayedTx, PlaceHolderSig).tx) @@ -113,7 +114,7 @@ class TransactionsSpec extends FunSuite with Logging { // MainPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(20000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val mainPenaltyTx = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(mainPenaltyTx, PlaceHolderSig).tx) @@ -125,10 +126,10 @@ class TransactionsSpec extends FunSuite with Logging { // HtlcPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubKeyScript = write(pay2wsh(redeemScript)) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, outputsAlreadyUsed = Set.empty, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(htlcPenaltyTx, PlaceHolderSig, localRevocationPriv.publicKey).tx) @@ -140,9 +141,9 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcSuccessTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) @@ -154,9 +155,9 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcTimeoutTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) @@ -176,21 +177,20 @@ class TransactionsSpec extends FunSuite with Logging { val remoteHtlcPriv = PrivateKey(randomBytes32 :+ 1.toByte) val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)) val commitInput = Funding.makeFundingInputInfo(randomBytes32, 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey) - val toLocalDelay = 144 - val localDustLimit = Satoshi(546) + val toLocalDelay = CltvExpiryDelta(144) + val localDustLimit = 546 sat val feeratePerKw = 22000 - // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32 - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, TestConstants.emptyOnionPacket) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliBtc(100).toMilliSatoshi, sha256(paymentPreimage1), CltvExpiry(300), TestConstants.emptyOnionPacket) val paymentPreimage2 = randomBytes32 - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, TestConstants.emptyOnionPacket) + val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliBtc(200).toMilliSatoshi, sha256(paymentPreimage2), CltvExpiry(300), TestConstants.emptyOnionPacket) // htlc3 and htlc4 are dust htlcs IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32 - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, TestConstants.emptyOnionPacket) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).toMilliSatoshi, sha256(paymentPreimage3), CltvExpiry(300), TestConstants.emptyOnionPacket) val paymentPreimage4 = randomBytes32 - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, TestConstants.emptyOnionPacket) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).toMilliSatoshi, sha256(paymentPreimage4), CltvExpiry(300), TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( DirectedHtlc(OUT, htlc1), @@ -199,8 +199,8 @@ class TransactionsSpec extends FunSuite with Logging { DirectedHtlc(IN, htlc4) ), feeratePerKw = feeratePerKw, - toLocalMsat = millibtc2satoshi(MilliBtc(400)).amount * 1000, - toRemoteMsat = millibtc2satoshi(MilliBtc(300)).amount * 1000) + toLocal = millibtc2satoshi(MilliBtc(400)).toMilliSatoshi, + toRemote = millibtc2satoshi(MilliBtc(300)).toMilliSatoshi) val commitTxNumber = 0x404142434445L val commitTx = { @@ -320,7 +320,7 @@ class TransactionsSpec extends FunSuite with Logging { } def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = - DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, TestConstants.emptyOnionPacket)) + DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.toMilliSatoshi, ByteVector32.Zeroes, CltvExpiry(144), TestConstants.emptyOnionPacket)) test("BOLT 2 fee tests") { @@ -341,7 +341,7 @@ class TransactionsSpec extends FunSuite with Logging { val htlcRegex = """.*HTLC ([a-z]+) amount ([0-9]+).*""".r - val dustLimit = Satoshi(546) + val dustLimit = 546 sat case class TestSetup(name: String, dustLimit: Satoshi, spec: CommitmentSpec, expectedFee: Satoshi) val tests = testRegex.findAllIn(bolt3).map(s => { @@ -353,7 +353,7 @@ class TransactionsSpec extends FunSuite with Logging { case "received" => htlc(IN, Satoshi(amount.toLong)) } }).toSet - TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocalMsat = to_local_msat.toLong, toRemoteMsat = to_remote_msat.toLong), Satoshi(fee.toLong)) + TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocal = MilliSatoshi(to_local_msat.toLong), toRemote = MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) }) // simple non-reg test making sure we are not missing tests diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 1f8432f80e..6a668347ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -16,23 +16,26 @@ package fr.acinq.eclair.wire +import java.net.InetSocketAddress import java.util.UUID import akka.actor.ActorSystem -import fr.acinq.bitcoin.Crypto.PrivateKey +import com.google.common.net.HostAndPort +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, MilliSatoshi, OutPoint, Satoshi, Transaction} -import fr.acinq.eclair._ +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain} import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.CommitTx +import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.ChannelCodecs._ +import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey, _} +import org.json4s.JsonAST._ import org.json4s.jackson.Serialization -import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey} +import org.json4s.{CustomKeySerializer, CustomSerializer} import org.scalatest.FunSuite import scodec.bits._ import scodec.{Attempt, DecodeResult} @@ -68,26 +71,26 @@ class ChannelCodecsSpec extends FunSuite { // before we had commitment version, public keys were stored first (they started with 0x02 and 0x03) val legacy02 = hex"02a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" val legacy03 = hex"03d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - val current02 = hex"010000000002a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" - val current03 = hex"010000000003d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" + val current02 = hex"010000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" + val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(channelVersionCodec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, legacy02.bits))) - assert(channelVersionCodec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, legacy03.bits))) + assert(channelVersionCodec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) + assert(channelVersionCodec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) - assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000000".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000001".bits)) } test("encode/decode localparams") { val o = LocalParams( nodeId = randomKey.publicKey, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), - toSelfDelay = Random.nextInt(Short.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), isFunder = Random.nextBoolean(), @@ -101,11 +104,11 @@ class ChannelCodecsSpec extends FunSuite { test("encode/decode remoteparams") { val o = RemoteParams( nodeId = randomKey.publicKey, - dustLimitSatoshis = Random.nextInt(Int.MaxValue), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserveSatoshis = Random.nextInt(Int.MaxValue), - htlcMinimumMsat = Random.nextInt(Int.MaxValue), - toSelfDelay = Random.nextInt(Short.MaxValue), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), fundingPubKey = randomKey.publicKey, revocationBasepoint = randomKey.publicKey, @@ -128,8 +131,8 @@ class ChannelCodecsSpec extends FunSuite { val add = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), - cltvExpiry = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), + cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add) @@ -142,15 +145,15 @@ class ChannelCodecsSpec extends FunSuite { val add1 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), - cltvExpiry = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), + cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), - amountMsat = Random.nextInt(Int.MaxValue), - cltvExpiry = Random.nextInt(Int.MaxValue), + amountMsat = MilliSatoshi(Random.nextInt(Int.MaxValue)), + cltvExpiry = CltvExpiry(Random.nextInt(Int.MaxValue)), paymentHash = randomBytes32, onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add1) @@ -160,8 +163,8 @@ class ChannelCodecsSpec extends FunSuite { val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), feeratePerKw = Random.nextInt(Int.MaxValue), - toLocalMsat = Random.nextInt(Int.MaxValue), - toRemoteMsat = Random.nextInt(Int.MaxValue) + toLocal = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toRemote = MilliSatoshi(Random.nextInt(Int.MaxValue)) ) val encoded = commitmentSpecCodec.encode(o).require val decoded = commitmentSpecCodec.decode(encoded).require @@ -170,19 +173,19 @@ class ChannelCodecsSpec extends FunSuite { test("encode/decode origin") { val id = UUID.randomUUID() - assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("system").deadLetters))).require).require === Local(id, None)) + assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("test").deadLetters))).require).require === Local(id, None)) // TODO: add backward compatibility check - val relayed = Relayed(randomBytes32, 4324, 12000000L, 11000000L) + val relayed = Relayed(randomBytes32, 4324, 12000000 msat, 11000000 msat) assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed) } test("encode/decode map of origins") { val map = Map( 1L -> Local(UUID.randomUUID(), None), - 42L -> Relayed(randomBytes32, 4324, 12000000L, 11000000L), - 130L -> Relayed(randomBytes32, -45, 13000000L, 12000000L), - 1000L -> Relayed(randomBytes32, 10, 14000000L, 13000000L), - -32L -> Relayed(randomBytes32, 54, 15000000L, 14000000L), + 42L -> Relayed(randomBytes32, 4324, 12000000 msat, 11000000 msat), + 130L -> Relayed(randomBytes32, -45, 13000000 msat, 12000000 msat), + 1000L -> Relayed(randomBytes32, 10, 14000000 msat, 13000000 msat), + -32L -> Relayed(randomBytes32, 54, 15000000 msat, 14000000 msat), -4L -> Local(UUID.randomUUID(), None)) assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) } @@ -300,41 +303,56 @@ class ChannelCodecsSpec extends FunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> - """{"$type":"fr.acinq.eclair.channel.DATA_NORMAL","commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":"m/1457788542/1007597768/1455922339/479707306","dustLimitSatoshis":"546","maxHtlcValueInFlightMsat":"5000000000","channelReserveSatoshis":"167772","htlcMinimumMsat":"1","toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":"573","maxHtlcValueInFlightMsat":"16609443000","channelReserveSatoshis":"167772","htlcMinimumMsat":"1000","toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":"7675","spec":{"htlcs":[],"feeratePerKw":"254","toLocalMsat":"204739729","toRemoteMsat":"16572475271"},"publishableTxs":{"commitTx":{"$type":"fr.acinq.eclair.transactions.Transactions.CommitTx","input":{"outPoint":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d:1","txOut":{"amount":{"$type":"fr.acinq.bitcoin.Satoshi","amount":"16777215"},"publicKeyScript":"002053bf7029eca054d1adf076c093c7a529144100108106ea0c4ca65fb686e2d384"},"redeemScript":"5221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae"},"tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":"7779","spec":{"htlcs":[],"feeratePerKw":"254","toLocalMsat":"16572475271","toRemoteMsat":"204739729"},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":"203","remoteNextHtlcId":"4147","originChannels":[],"remoteNextCommitInfo":[1,"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6"],"commitInput":{"outPoint":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d:1","txOut":{"amount":{"$type":"fr.acinq.bitcoin.Satoshi","amount":"16777215"},"publicKeyScript":"002053bf7029eca054d1adf076c093c7a529144100108106ea0c4ca65fb686e2d384"},"redeemScript":"5221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae"},"remotePerCommitmentSecrets":{"knownHashes":[["-7","e1b9c9b10a97f15f5e1bb27fc8ac670df8dadeae4edfafb23bdd0ac705fdf516"],["-3","4daa7562599a7a1c9314483edacdb0907bcf3bda2b80c58587c12a7b8fabfcd8"],["-7779","531d1f83f60a42fa28ee2cfdcc05689851e69c5e0334e82a49c08d392ce51027"],["-15","ca4e125e9126107745d4354d4187abcde323117857a1dceb7ccf60b2aafa80c6"],["-243","edb314aeb499bfa1c7e89a37679a7775e77a09871fda873db21b11bf2655252d"],["-1","ad2581f3494a17b0be3f63516d53f028a204fd3156d8b21aa4e57a8738d20620"],["-121","26055d7f66069cea12330090f2d19efd147da0476bd35c72b633a2c7a5b6c643"],["-3889","dc96b9882aae674484e10520e6700a34bb43cf94c8650814aa51d95ff47f6c51"]],"lastIndex":["281474976702877"]},"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":[{"$type":"fr.acinq.eclair.wire.ChannelAnnouncement","nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"}],"channelUpdate":{"$type":"fr.acinq.eclair.wire.ChannelUpdate","signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":"1560862173","messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":"1000","feeBaseMsat":"1000","feeProportionalMillionths":"100","htlcMaximumMsat":["16777215000"]},"localShutdown":[],"remoteShutdown":[]}""", - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> - """{"$type":"fr.acinq.eclair.channel.DATA_NORMAL","commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":"m/1413737705'/1506032145'/563828043'/715566357'","dustLimitSatoshis":"546","maxHtlcValueInFlightMsat":"1000000000","channelReserveSatoshis":"150000","htlcMinimumMsat":"1","toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":"573","maxHtlcValueInFlightMsat":"14850000000","channelReserveSatoshis":"150000","htlcMinimumMsat":"1000","toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":"20024","spec":{"htlcs":[],"feeratePerKw":"750","toLocalMsat":"1343316620","toRemoteMsat":"13656683380"},"publishableTxs":{"commitTx":{"$type":"fr.acinq.eclair.transactions.Transactions.CommitTx","input":{"outPoint":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611:0","txOut":{"amount":{"$type":"fr.acinq.bitcoin.Satoshi","amount":"15000000"},"publicKeyScript":"0020b084836bf66c8411edcc9501a22cdb5b0cc30d621802bda0507fe386118420b4"},"redeemScript":"52210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52ae"},"tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":"20024","spec":{"htlcs":[],"feeratePerKw":"750","toLocalMsat":"13656683380","toRemoteMsat":"1343316620"},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":"9288","remoteNextHtlcId":"151","originChannels":[],"remoteNextCommitInfo":[1,"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16"],"commitInput":{"outPoint":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611:0","txOut":{"amount":{"$type":"fr.acinq.bitcoin.Satoshi","amount":"15000000"},"publicKeyScript":"0020b084836bf66c8411edcc9501a22cdb5b0cc30d621802bda0507fe386118420b4"},"redeemScript":"52210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52ae"},"remotePerCommitmentSecrets":{"knownHashes":[["-39","7947ac7e448620879a02d4392c8b7e2c250e16f4ceb2b8daff27a30bd6b4b695"],["-2503","192450ea26fd1d64f611bea29649b2006554374a8be6679547bbdba49e6b85b5"],["-19","413bd09c4192825cec14dc9f0f99cd4b4038972bbb6513d41e091273aba3d425"],["-1","588ad62c5d554800cac49f51d955a0469569a456823f1db658af2ccb12f1c304"],["-9","33c39a21a38bb61f6fb33623771a9356d8885b7c12c939c770c939ef826286c2"],["-625","8f84d5aa86acb1a0c7d79ee74ba1ddfce7dad6e684c48dd1bf67443a3898e8ca"],["-1251","c13414b235c83d4bfde8a2af54339d095c73d53753c87bc0174aae71db4ef2bf"]],"lastIndex":["281474976690632"]},"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelAnnouncement":[],"channelUpdate":{"$type":"fr.acinq.eclair.wire.ChannelUpdate","signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":"1561369173","messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":"1000","feeBaseMsat":"1000","feeProportionalMillionths":"100","htlcMaximumMsat":["15000000000"]},"localShutdown":[],"remoteShutdown":[]}""" + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserveSatoshis":167772,"htlcMinimumMsat":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":204739729,"toRemoteMsat":16572475271},"publishableTxs":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"0200000000010107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce1112040047304402204d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a8022065845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d01483045022100f968fb38342997065f66c38731d4ce592a85e6952175a8511f4d58bf93d308b8022039ee395d24a5a71226bb18c0c44bb9e3c066799be3f5d096e9f8ba7a88184bf101475221028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b642103660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e352ae7af8a620"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"feeratePerKw":254,"toLocalMsat":16572475271,"toRemoteMsat":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,"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c"},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":"","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3"},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":1560862173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000}}""", + hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"commitments":{"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","channelKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimitSatoshis":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","globalFeatures":"","localFeatures":"8a"},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimitSatoshis":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserveSatoshis":150000,"htlcMinimumMsat":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","globalFeatures":"","localFeatures":"81"},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":1343316620,"toRemoteMsat":13656683380},"publishableTxs":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"020000000001016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f490400483045022100bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af6231702203b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f01483045022100e9e60db46ea3709d8bff62ab7b94f71c0441177b791a9e664f574aaf7e4f9a2e02205de95919925e8d3b52c85a9a82c578a8bf16695bc2b6fadf330115445610d603014752210215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc42103bd15bf4221b91529b173d3dec2d75d0b3050f91f055fbe1f80d0d2faae04cdfb52aedf013320"},"htlcTxsAndSigs":[]}},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocalMsat":13656683380,"toRemoteMsat":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,"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611"},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000}}""" ) refs.foreach { case (oldbin, refjson) => // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value.asInstanceOf[DATA_NORMAL] + 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.asInstanceOf[DATA_NORMAL] + 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) - import JsonSerializers._ - val oldjson = upickle.default.write(oldnormal).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") - val newjson = upickle.default.write(newnormal).replace(""","unknownFields":""""", "").replace(""""channelVersion":"00000000000000000000000000000000",""", "") + val oldjson = Serialization.write(oldnormal)(JsonSupport.formats) + .replace(""","unknownFields":""""", "") + .replace(""""channelVersion":"00000000000000000000000000000000",""", "") + .replace(""""dustLimit"""", """"dustLimitSatoshis"""") + .replace(""""channelReserve"""", """"channelReserveSatoshis"""") + .replace(""""htlcMinimum"""", """"htlcMinimumMsat"""") + .replace(""""toLocal"""", """"toLocalMsat"""") + .replace(""""toRemote"""", """"toRemoteMsat"""") + .replace("fundingKeyPath", "channelKeyPath") + .replace(""""version":0,""", "") + + val newjson = Serialization.write(newnormal)(JsonSupport.formats) + .replace(""","unknownFields":""""", "") + .replace(""""channelVersion":"00000000000000000000000000000000",""", "") + .replace(""""dustLimit"""", """"dustLimitSatoshis"""") + .replace(""""channelReserve"""", """"channelReserveSatoshis"""") + .replace(""""htlcMinimum"""", """"htlcMinimumMsat"""") + .replace(""""toLocal"""", """"toLocalMsat"""") + .replace(""""toRemote"""", """"toRemoteMsat"""") + .replace("fundingKeyPath", "channelKeyPath") + .replace(""""version":0,""", "") + assert(oldjson === refjson) assert(newjson === refjson) } - } - } object ChannelCodecsSpec { val keyManager = new LocalKeyManager(ByteVector32(ByteVector.fill(32)(1)), Block.RegtestGenesisBlock.hash) val localParams = LocalParams( keyManager.nodeId, - channelKeyPath = DeterministicWallet.KeyPath(Seq(42L)), - dustLimitSatoshis = Satoshi(546).toLong, + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - channelReserveSatoshis = 10000, - htlcMinimumMsat = 10000, - toSelfDelay = 144, + channelReserve = 10000 sat, + htlcMinimum = 10000 msat, + toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, isFunder = true, @@ -343,11 +361,11 @@ object ChannelCodecsSpec { val remoteParams = RemoteParams( nodeId = randomKey.publicKey, - dustLimitSatoshis = Satoshi(546).toLong, + dustLimit = 546 sat, maxHtlcValueInFlightMsat = UInt64(5000000), - channelReserveSatoshis = 10000, - htlcMinimumMsat = 5000, - toSelfDelay = 144, + channelReserve = 10000 sat, + htlcMinimum = 5000 msat, + toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, fundingPubKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)) :+ 1.toByte).publicKey, revocationBasepoint = PrivateKey(ByteVector.fill(32)(2)).publicKey, @@ -366,27 +384,178 @@ object ChannelCodecsSpec { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, 1000000 msat, Crypto.sha256(paymentPreimages(0)), CltvExpiry(500), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000000 msat, Crypto.sha256(paymentPreimages(1)), CltvExpiry(501), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, 2000000 msat, Crypto.sha256(paymentPreimages(2)), CltvExpiry(502), TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, 3000000 msat, Crypto.sha256(paymentPreimages(3)), CltvExpiry(503), TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) ) val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") val fundingAmount = fundingTx.txOut(0).amount - val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey) + val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey) - val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000, 70000000), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) - val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000, 700000), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) + val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, 1500, 50000000 msat, 70000000 msat), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)) + val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(htlc => htlc.copy(direction = htlc.direction.opposite)).toSet, 1500, 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val commitments = Commitments(ChannelVersion.STANDARD, localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, - originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), + originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000 msat, 10000000 msat)), remoteNextCommitInfo = Right(randomKey.publicKey), commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes) - val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), 42, 15, 575, 53, Channel.MAX_FUNDING_SATOSHIS * 1000L) + val channelUpdate = Announcements.makeChannelUpdate(ByteVector32(ByteVector.fill(32)(1)), randomKey, randomKey.publicKey, ShortChannelId(142553), CltvExpiryDelta(42), 15 msat, 575 msat, 53, Channel.MAX_FUNDING.toMilliSatoshi) val normal = DATA_NORMAL(commitments, ShortChannelId(42), true, None, channelUpdate, None, None) -} + + object JsonSupport { + + class ByteVectorSerializer extends CustomSerializer[ByteVector](format => ( { + null + }, { + case x: ByteVector => JString(x.toHex) + })) + + class ByteVector32Serializer extends CustomSerializer[ByteVector32](format => ( { + null + }, { + case x: ByteVector32 => JString(x.toHex) + })) + + class ByteVector64Serializer extends CustomSerializer[ByteVector64](format => ( { + null + }, { + case x: ByteVector64 => JString(x.toHex) + })) + + class UInt64Serializer extends CustomSerializer[UInt64](format => ( { + null + }, { + case x: UInt64 => JInt(x.toBigInt) + })) + + class SatoshiSerializer extends CustomSerializer[Satoshi](format => ( { + null + }, { + case x: Satoshi => JInt(x.toLong) + })) + + class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ( { + null + }, { + case x: MilliSatoshi => JInt(x.toLong) + })) + + class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ( { + null + }, { + case x: CltvExpiry => JLong(x.toLong) + })) + + class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](format => ( { + null + }, { + case x: CltvExpiryDelta => JInt(x.toInt) + })) + + class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ( { + null + }, { + case x: ShortChannelId => JString(x.toString()) + })) + + class StateSerializer extends CustomSerializer[State](format => ( { + null + }, { + case x: State => JString(x.toString()) + })) + + class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( { + null + }, { + case x: ShaChain => JNull + })) + + class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( { + null + }, { + case x: PublicKey => JString(x.toString()) + })) + + class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( { + null + }, { + case x: PrivateKey => JString("XXX") + })) + + class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](format => ( { + null + }, { + case x: ChannelVersion => JString(x.bits.toBin) + })) + + class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ( { + null + }, { + case x: Transaction => JObject(List( + JField("txid", JString(x.txid.toHex)), + JField("tx", JString(x.toString())) + )) + })) + + class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ( { + null + }, { + case x: TransactionWithInputInfo => JObject(List( + JField("txid", JString(x.tx.txid.toHex)), + JField("tx", JString(x.tx.toString())) + )) + })) + + class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](format => ( { + null + }, { + case address: InetSocketAddress => JString(HostAndPort.fromParts(address.getHostString, address.getPort).toString) + })) + + class OutPointSerializer extends CustomSerializer[OutPoint](format => ( { + null + }, { + case x: OutPoint => JString(s"${x.txid}:${x.index}") + })) + + class OutPointKeySerializer extends CustomKeySerializer[OutPoint](format => ( { + null + }, { + case x: OutPoint => s"${x.txid}:${x.index}" + })) + + class InputInfoSerializer extends CustomSerializer[InputInfo](format => ( { + null + }, { + case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong))) + })) + + implicit val formats = org.json4s.DefaultFormats + + new ByteVectorSerializer + + new ByteVector32Serializer + + new ByteVector64Serializer + + new UInt64Serializer + + new SatoshiSerializer + + new MilliSatoshiSerializer + + new CltvExpirySerializer + + new CltvExpiryDeltaSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new ChannelVersionSerializer + + new InputInfoSerializer + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala new file mode 100644 index 0000000000..91c5f3c8f6 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ExtendedQueriesCodecsSpec.scala @@ -0,0 +1,153 @@ +/* + * 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.wire + +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.router.Router +import fr.acinq.eclair.wire.LightningMessageCodecs._ +import fr.acinq.eclair.wire.ReplyChannelRangeTlv._ +import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, UInt64} +import org.scalatest.FunSuite +import scodec.bits.ByteVector + +class ExtendedQueriesCodecsSpec extends FunSuite { + 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))), + TlvStream.empty) + + val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require + val decoded = queryShortChannelIdsCodec.decode(encoded).require.value + 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))), + 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) + } + + 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))), + TlvStream( + QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)) :: Nil, + GenericTlv(UInt64(43), ByteVector.fromValidHex("deadbeef")) :: Nil + ) + ) + + val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require + val decoded = queryShortChannelIdsCodec.decode(encoded).require.value + assert(decoded === query_short_channel_id) + } + + test("encode reply_channel_range (no optional data)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + None, None) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } + + test("encode reply_channel_range (with optional timestamps)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3)))), + None) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } + + test("encode reply_channel_range (with optional timestamps, checksums, and unknown data)") { + val replyChannelRange = ReplyChannelRange( + Block.RegtestGenesisBlock.blockId, + 1, 100, + 1.toByte, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream( + List( + EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))), + EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) + ), + GenericTlv(UInt64(7), ByteVector.fromValidHex("deadbeef")) :: Nil + ) + ) + + val encoded = replyChannelRangeCodec.encode(replyChannelRange).require + val decoded = replyChannelRangeCodec.decode(encoded).require.value + assert(decoded === replyChannelRange) + } + + test("compute checksums correctly (CL test #1)") { + val update = ChannelUpdate( + chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), + signature = ByteVector64.fromValidHex("76df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c80293282"), + shortChannelId = ShortChannelId("103x1x0"), + timestamp = 1565587763L, + messageFlags = 0, + channelFlags = 0, + cltvExpiryDelta = CltvExpiryDelta(144), + htlcMinimumMsat = 0 msat, + htlcMaximumMsat = None, + feeBaseMsat = 1000 msat, + feeProportionalMillionths = 10 + ) + val check = ByteVector.fromValidHex("010276df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c8029328206226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006700000100005d50f933000000900000000000000000000003e80000000a") + assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2)) + + val checksum = Router.getChecksum(update) + assert(checksum == 0x1112fa30L) + } + + test("compute checksums correctly (CL test #2)") { + val update = ChannelUpdate( + chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), + signature = ByteVector64.fromValidHex("06737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c"), + shortChannelId = ShortChannelId("109x1x0"), + timestamp = 1565587765L, + messageFlags = 1, + channelFlags = 0, + cltvExpiryDelta = CltvExpiryDelta(48), + htlcMinimumMsat = 0 msat, + htlcMaximumMsat = Some(100000 msat), + feeBaseMsat = 100 msat, + feeProportionalMillionths = 11 + ) + val check = ByteVector.fromValidHex("010206737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f00006d00000100005d50f935010000300000000000000000000000640000000b00000000000186a0") + assert(LightningMessageCodecs.channelUpdateCodec.encode(update).require.bytes == check.drop(2)) + + val checksum = Router.getChecksum(update) + assert(checksum == 0xf32ce968L) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 3b3ecd7a61..3925b55044 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -18,14 +18,14 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Hmac256 -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} import fr.acinq.eclair.wire.FailureMessageCodecs._ +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, UInt64, randomBytes32, randomBytes64} import org.scalatest.FunSuite import scodec.bits._ /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ class FailureMessageCodecsSpec extends FunSuite { val channelUpdate = ChannelUpdate( @@ -33,21 +33,21 @@ class FailureMessageCodecsSpec extends FunSuite { chainHash = Block.RegtestGenesisBlock.hash, shortChannelId = ShortChannelId(12345), timestamp = 1234567L, - cltvExpiryDelta = 100, + cltvExpiryDelta = CltvExpiryDelta(100), messageFlags = 0, channelFlags = 1, - htlcMinimumMsat = 1000, - feeBaseMsat = 12, + htlcMinimumMsat = 1000 msat, + feeBaseMsat = 12 msat, feeProportionalMillionths = 76, htlcMaximumMsat = None) - test("encode/decode all channel messages") { + test("encode/decode all failure messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionPayload(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: - AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: - IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil + AmountBelowMinimum(123456 msat, channelUpdate) :: FeeInsufficient(546463 msat, channelUpdate) :: IncorrectCltvExpiry(CltvExpiry(1211), channelUpdate) :: ExpiryTooSoon(channelUpdate) :: + IncorrectOrUnknownPaymentDetails(123456 msat, 1105) :: FinalIncorrectCltvExpiry(CltvExpiry(1234)) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: InvalidOnionPayload(UInt64(561), 1105) :: Nil msgs.foreach { msg => { @@ -58,16 +58,36 @@ class FailureMessageCodecsSpec extends FunSuite { } } + test("decode unknown failure messages") { + val testCases = Seq( + // Deprecated incorrect_payment_amount. + (false, true, hex"4010"), + // Deprecated final_expiry_too_soon. + (false, true, hex"4011"), + // Unknown failure messages. + (false, false, hex"00ff 42"), + (true, false, hex"20ff 42"), + (true, true, hex"60ff 42") + ) + + for ((node, perm, bin) <- testCases) { + 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) + } + } + test("bad onion failure code") { val msgs = Map( (BADONION | PERM | 4) -> InvalidOnionVersion(randomBytes32), (BADONION | PERM | 5) -> InvalidOnionHmac(randomBytes32), - (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32), - (BADONION | PERM) -> InvalidOnionPayload(randomBytes32) + (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32) ) for ((code, message) <- msgs) { - assert(failureCode(message) === code) + assert(message.code === code) } } @@ -75,7 +95,7 @@ class FailureMessageCodecsSpec extends FunSuite { val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) val testCases = Map( InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) -> hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - IncorrectOrUnknownPaymentDetails(42) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + IncorrectOrUnknownPaymentDetails(42 msat, 1105) -> hex"5eb766da1b2f45b4182e064dacd8da9eca2c9a33f0dce363ff308e9bdb3ee4e3 000e 400f 000000000000002a 00000451 00f2 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) for ((expected, bin) <- testCases) { @@ -87,6 +107,22 @@ class FailureMessageCodecsSpec extends FunSuite { } } + test("decode backwards-compatible IncorrectOrUnknownPaymentDetails") { + val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) + val testCases = Map( + // Without any data. + IncorrectOrUnknownPaymentDetails(MilliSatoshi(0), 0) -> hex"0d83b55dd5a6086e4033c3659125ed1ff436964ce0e67ed5a03bddb16a9a1041 0002 400f 00fe 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // With an amount but no height. + IncorrectOrUnknownPaymentDetails(MilliSatoshi(42), 0) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // With amount and height. + IncorrectOrUnknownPaymentDetails(MilliSatoshi(42), 1105) -> hex"5eb766da1b2f45b4182e064dacd8da9eca2c9a33f0dce363ff308e9bdb3ee4e3 000e 400f 000000000000002a 00000451 00f2 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + + for ((expected, bin) <- testCases) { + assert(codec.decode(bin.bits).require.value === expected) + } + } + test("decode invalid failure onion packet") { val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) val testCases = Seq( @@ -112,7 +148,7 @@ class FailureMessageCodecsSpec extends FunSuite { test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" - val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, 1000, 1, 1, None)) + val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, CltvExpiryDelta(14), 1000 msat, 1 msat, 1, None)) val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value assert(u === ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index ff2c26b5a2..03f01c3d59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -23,12 +23,13 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ +import ReplyChannelRangeTlv._ import org.scalatest.FunSuite import scodec.bits.{ByteVector, HexStringSyntax} /** - * Created by PM on 31/05/2016. - */ + * Created by PM on 31/05/2016. + */ class LightningMessageCodecsSpec extends FunSuite { @@ -52,15 +53,15 @@ class LightningMessageCodecsSpec extends FunSuite { } test("encode/decode all channel messages") { - val open = OpenChannel(randomBytes32, randomBytes32, 3, 4, 5, UInt64(6), 7, 8, 9, 10, 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) - val accept = AcceptChannel(randomBytes32, 3, UInt64(4), 5, 6, 7, 8, 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val open = OpenChannel(randomBytes32, randomBytes32, 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, 9, 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, 2) val shutdown = Shutdown(randomBytes32, bin(47, 0)) - val closing_signed = ClosingSigned(randomBytes32, 2, randomBytes64) - val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, TestConstants.emptyOnionPacket) + 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) @@ -68,12 +69,21 @@ class LightningMessageCodecsSpec extends FunSuite { val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, 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, 42, 0, 3, 4, 5, 6, None) + val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 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, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, randomBytes(7515)) - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, randomBytes(3200)) + 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, + 100000, + 1500, + TlvStream(QueryChannelRangeTlv.QueryFlags((QueryChannelRangeTlv.QueryFlags.WANT_ALL)) :: Nil, unknownTlv :: Nil)) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream( + EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1, 1), Timestamps(2, 2), Timestamps(3, 3))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, + unknownTlv :: Nil) + ) val ping = Ping(100, bin(10, 1)) val pong = Pong(bin(10, 1)) val channel_reestablish = ChannelReestablish(randomBytes32, 242842L, 42L) @@ -92,11 +102,125 @@ class LightningMessageCodecsSpec extends FunSuite { } } + 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), + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(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)))), + 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)))) + ) + + refs.forall { + case (bin, obj) => + lightningMessageCodec.decode(bin.toBitVector).require.value == obj && lightningMessageCodec.encode(obj).require == bin.toBitVector + } + } + + case class TestItem(msg: Any, hex: String) + + test("test vectors for extended channel queries ") { + val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, 100000, 1500, TlvStream.empty) + val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, + 35000, + 100, + TlvStream(QueryChannelRangeTlv.QueryFlags((QueryChannelRangeTlv.QueryFlags.WANT_ALL)))) + val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 756230, 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) + val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 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, 122334, 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) + val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, 122334, 1500, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545, 948165), Timestamps(489645, 4786864), Timestamps(46456, 9788415)))), + 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" + ) + + val items = refs.map { case (obj, refbin) => + val bin = lightningMessageCodec.encode(obj).require + assert(refbin.bits === bin) + TestItem(obj, bin.toHex) + } + + // NB: uncomment this to update the test vectors + + /*class EncodingTypeSerializer extends CustomSerializer[EncodingType](format => ( { + null + }, { + case EncodingType.UNCOMPRESSED => JString("UNCOMPRESSED") + case EncodingType.COMPRESSED_ZLIB => JString("COMPRESSED_ZLIB") + })) + + class ExtendedQueryFlagsSerializer extends CustomSerializer[QueryChannelRangeTlv.QueryFlags](format => ( { + null + }, { + case QueryChannelRangeTlv.QueryFlags(flag) => + JString(((if (QueryChannelRangeTlv.QueryFlags.wantTimestamps(flag)) List("WANT_TIMESTAMPS") else List()) ::: (if (QueryChannelRangeTlv.QueryFlags.wantChecksums(flag)) List("WANT_CHECKSUMS") else List())) mkString (" | ")) + })) + + implicit val formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + + new EncodingTypeSerializer + + new ExtendedQueryFlagsSerializer + + new ByteVectorSerializer + + new ByteVector32Serializer + + new UInt64Serializer + + new MilliSatoshiSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new InputInfoSerializer + + new ColorSerializer + + new RouteResponseSerializer + + new ThrowableSerializer + + new FailureMessageSerializer + + new NodeAddressSerializer + + new DirectionSerializer + + new PaymentRequestSerializer + + ShortTypeHints(List( + classOf[QueryChannelRange], + classOf[ReplyChannelRange], + classOf[QueryShortChannelIds])) + + val json = Serialization.writePretty(items) + println(json)*/ + } + test("decode channel_update with htlc_maximum_msat") { // 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, 1, 1, 6, 1, 1, 10, Some(980000000L))) + assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 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) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala index 763a9ce42a..c2077d0ebb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -17,14 +17,18 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.UInt64.Conversions._ +import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, RelayLegacyPayload, RelayTlvPayload} import fr.acinq.eclair.wire.OnionCodecs._ +import fr.acinq.eclair.wire.OnionTlv._ +import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, ShortChannelId, UInt64} import org.scalatest.FunSuite +import scodec.Attempt import scodec.bits.HexStringSyntax /** - * Created by t-bast on 05/07/2019. - */ + * Created by t-bast on 05/07/2019. + */ class OnionCodecsSpec extends FunSuite { @@ -39,18 +43,35 @@ class OnionCodecsSpec extends FunSuite { assert(encoded.toByteVector === bin) } - test("encode/decode per-hop payload") { - val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) - val bin = perHopPayloadCodec.encode(payload).require - assert(bin.toByteVector.size === 33) - val payload1 = perHopPayloadCodec.decode(bin).require.value - assert(payload === payload1) - - // realm (the first byte) should be 0 - val bin1 = bin.toByteVector.update(0, 1) - intercept[IllegalArgumentException] { - val payload2 = perHopPayloadCodec.decode(bin1.bits).require.value - assert(payload2 === payload1) + test("encode/decode fixed-size (legacy) relay per-hop payload") { + val testCases = Map( + RelayLegacyPayload(ShortChannelId(0), 0 msat, CltvExpiry(0)) -> hex"00 0000000000000000 0000000000000000 00000000 000000000000000000000000", + RelayLegacyPayload(ShortChannelId(42), 142000 msat, CltvExpiry(500000)) -> hex"00 000000000000002a 0000000000022ab0 0007a120 000000000000000000000000", + RelayLegacyPayload(ShortChannelId(561), 1105 msat, CltvExpiry(1729)) -> hex"00 0000000000000231 0000000000000451 000006c1 000000000000000000000000" + ) + + for ((expected, bin) <- testCases) { + val decoded = relayPerHopPayloadCodec.decode(bin.bits).require.value + assert(decoded === expected) + + val encoded = relayPerHopPayloadCodec.encode(expected).require.bytes + assert(encoded === bin) + } + } + + test("encode/decode fixed-size (legacy) final per-hop payload") { + val testCases = Map( + FinalLegacyPayload(0 msat, CltvExpiry(0)) -> hex"00 0000000000000000 0000000000000000 00000000 000000000000000000000000", + FinalLegacyPayload(142000 msat, CltvExpiry(500000)) -> hex"00 0000000000000000 0000000000022ab0 0007a120 000000000000000000000000", + FinalLegacyPayload(1105 msat, CltvExpiry(1729)) -> hex"00 0000000000000000 0000000000000451 000006c1 000000000000000000000000" + ) + + for ((expected, bin) <- testCases) { + val decoded = finalPerHopPayloadCodec.decode(bin.bits).require.value + assert(decoded === expected) + + val encoded = finalPerHopPayloadCodec.encode(expected).require.bytes + assert(encoded === bin) } } @@ -71,4 +92,88 @@ class OnionCodecsSpec extends FunSuite { } } + test("encode/decode variable-length (tlv) relay per-hop payload") { + val testCases = Map( + TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451", + TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"17 02020231 04012a 06080000000000000451 fdffff0206c1" + ) + + for ((expected, bin) <- testCases) { + val decoded = relayPerHopPayloadCodec.decode(bin.bits).require.value + assert(decoded === RelayTlvPayload(expected)) + assert(decoded.amountToForward === 561.msat) + assert(decoded.outgoingCltv === CltvExpiry(42)) + assert(decoded.outgoingChannelId === ShortChannelId(1105)) + + val encoded = relayPerHopPayloadCodec.encode(RelayTlvPayload(expected)).require.bytes + assert(encoded === bin) + } + } + + test("encode/decode variable-length (tlv) final per-hop payload") { + val testCases = Map( + TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42))) -> hex"07 02020231 04012a", + TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451", + TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"0d 02020231 04012a fdffff0206c1" + ) + + 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)) + + val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(expected)).require.bytes + assert(encoded === bin) + } + } + + test("decode variable-length (tlv) relay per-hop payload missing information") { + val testCases = Seq( + (InvalidOnionPayload(UInt64(2), 0), hex"0d 04012a 06080000000000000451"), // missing amount + (InvalidOnionPayload(UInt64(4), 0), hex"0e 02020231 06080000000000000451"), // missing cltv + (InvalidOnionPayload(UInt64(6), 0), hex"07 02020231 04012a") // missing channel id + ) + + for ((expectedErr, bin) <- testCases) { + val decoded = relayPerHopPayloadCodec.decode(bin.bits) + assert(decoded.isFailure) + val Attempt.Failure(err: MissingRequiredTlv) = decoded + assert(err.failureMessage === expectedErr) + } + } + + test("decode variable-length (tlv) final per-hop payload missing information") { + val testCases = Seq( + (InvalidOnionPayload(UInt64(2), 0), hex"03 04012a"), // missing amount + (InvalidOnionPayload(UInt64(4), 0), hex"04 02020231") // missing cltv + ) + + for ((expectedErr, bin) <- testCases) { + val decoded = finalPerHopPayloadCodec.decode(bin.bits) + assert(decoded.isFailure) + val Attempt.Failure(err: MissingRequiredTlv) = decoded + assert(err.failureMessage === expectedErr) + } + } + + test("decode invalid per-hop payload") { + val testCases = Seq( + // Invalid fixed-size (legacy) payload. + hex"00 000000000000002a 000000000000002a", // invalid length + // Invalid variable-length (tlv) payload. + hex"00", // empty payload is missing required information + hex"01", // invalid length + hex"01 0000", // invalid length + hex"04 0000 2a00", // unknown even types + hex"04 0000 0000", // duplicate types + hex"04 0100 0000" // unordered types + ) + + for (testCase <- testCases) { + assert(relayPerHopPayloadCodec.decode(testCase.bits).isFailure) + assert(finalPerHopPayloadCodec.decode(testCase.bits).isFailure) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala index e2c5dbce1b..aaf5e4d1aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/TlvCodecsSpec.scala @@ -116,6 +116,17 @@ class TlvCodecsSpec extends FunSuite { } } + test("encode/decode truncated uint64 overflow") { + assert(tu64overflow.encode(Long.MaxValue).require.toByteVector === hex"087fffffffffffffff") + assert(tu64overflow.decode(hex"087fffffffffffffff".bits).require.value === Long.MaxValue) + + assert(tu64overflow.encode(42L).require.toByteVector === hex"012a") + assert(tu64overflow.decode(hex"012a".bits).require.value === 42L) + + assert(tu64overflow.encode(-1L).isFailure) + assert(tu64overflow.decode(hex"088000000000000000".bits).isFailure) + } + test("decode invalid truncated integers") { val testCases = Seq( (tu16, hex"01 00"), // not minimal @@ -294,6 +305,12 @@ class TlvCodecsSpec extends FunSuite { } } + 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) + } } object TlvCodecsSpec { diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 47a0f8c584..c66f1d1bb2 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -80,6 +80,7 @@ classutil_${scala.version.short} 1.4.0 + ch.qos.logback logback-classic @@ -91,10 +92,38 @@ janino 3.0.7 + - com.googlecode.lanterna - lanterna - 3.0.0-rc1 + io.spray + spray-can_${scala.version.short} + 1.3.3 + + + io.spray + spray-http_${scala.version.short} + 1.3.3 + + + io.spray + spray-httpx_${scala.version.short} + 1.3.3 + + + io.spray + spray-routing-shapeless2_${scala.version.short} + 1.3.3 + + + io.spray + spray-util_${scala.version.short} + 1.3.3 + + + + org.mockito + mockito-scala-scalatest_2.11 + 1.4.1 + test diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index 3241994b3b..bd5b8957bb 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -18,8 +18,12 @@ package fr.acinq.eclair import java.io.File -import akka.actor.ActorSystem +import akka.actor.{ActorSystem, Props, SupervisorStrategy} +import akka.io.IO +import com.typesafe.config.Config +import fr.acinq.eclair.api.Service import grizzled.slf4j.Logging +import spray.can.Http import scala.concurrent.ExecutionContext import scala.util.{Failure, Success} @@ -37,15 +41,40 @@ object Boot extends App with Logging { implicit val system: ActorSystem = ActorSystem("eclair-node") implicit val ec: ExecutionContext = system.dispatcher val setup = new Setup(datadir) + plugins.foreach(_.onSetup(setup)) setup.bootstrap onComplete { - case Success(kit) => plugins.foreach(_.onKit(kit)) + case Success(kit) => + startApiServiceIfEnabled(setup.config, kit) + plugins.foreach(_.onKit(kit)) case Failure(t) => onError(t) } } catch { case t: Throwable => onError(t) } + /** + * Starts the http APIs service if enabled in the configuration + * + * @param config + * @param kit + * @param system + * @param ec + */ + def startApiServiceIfEnabled(config: Config, kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = { + if(config.getBoolean("api.enabled")){ + logger.info(s"json API enabled on port=${config.getInt("api.port")}") + val apiPassword = config.getString("api.password") match { + case "" => throw EmptyAPIPasswordException + case valid => valid + } + val serviceActor = system.actorOf(SimpleSupervisor.props(Props(new Service(apiPassword, new EclairImpl(kit))), "api-service", SupervisorStrategy.Restart)) + IO(Http) ! Http.Bind(serviceActor, config.getString("api.binding-ip"), config.getInt("api.port")) + } else { + logger.info("json API disabled") + } + } + def onError(t: Throwable): Unit = { val errorMsg = if (t.getMessage != null) t.getMessage else t.getClass.getSimpleName System.err.println(s"fatal error: $errorMsg") diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Textui.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Textui.scala deleted file mode 100644 index 0b24a2b06d..0000000000 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Textui.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2018 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 - -import java.util.concurrent.atomic.AtomicBoolean - -import akka.actor.{ActorRef, Props, SupervisorStrategy} -import akka.pattern.ask -import akka.util.Timeout -import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder -import com.googlecode.lanterna.input.KeyStroke -import com.googlecode.lanterna.{TerminalPosition, TerminalSize} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{MilliSatoshi, Satoshi} -import fr.acinq.eclair.channel.State -import fr.acinq.eclair.io.{NodeURI, Peer} -import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment -import fr.acinq.eclair.payment.PaymentRequest -import grizzled.slf4j.Logging -import scodec.bits.ByteVector - -import scala.collection.JavaConversions._ -import scala.concurrent.duration._ - -/** - * Created by PM on 05/06/2017. - */ -class Textui(kit: Kit) extends Logging { - - import com.googlecode.lanterna.TextColor - import com.googlecode.lanterna.gui2._ - import com.googlecode.lanterna.screen.TerminalScreen - import com.googlecode.lanterna.terminal.DefaultTerminalFactory - - // Setup terminal and screen layers - val terminal = new DefaultTerminalFactory().createTerminal - val screen = new TerminalScreen(terminal) - screen.startScreen() - - // Create panel to hold components - val mainPanel = new Panel() - mainPanel.setLayoutManager(new BorderLayout()) - - val channelsPanel = new Panel() - channelsPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL)) - channelsPanel.setLayoutData(BorderLayout.Location.TOP) - mainPanel.addComponent(channelsPanel) - channelsPanel.addComponent(new Label("channels")) - - val channels = collection.mutable.Map[ActorRef, Panel]() - - def addChannel(channel: ActorRef, channelId: ByteVector, remoteNodeId: PublicKey, state: State, balance: Satoshi, capacity: Satoshi): Unit = { - val channelPanel = new Panel() - channelPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)) - val channelDataPanel = new Panel() - channelDataPanel.setLayoutManager(new GridLayout(2)) - channelDataPanel.addComponent(new Label(s"$channelId")) - channelDataPanel.addComponent(new Label(s"${state.toString}")) - channelDataPanel.addComponent(new Label(s"$remoteNodeId")) - channelDataPanel.addComponent(new EmptySpace(new TerminalSize(0, 0))) // Empty space underneath labels - channelDataPanel.addComponent(new Separator(Direction.HORIZONTAL)) // Empty space underneath labels - channelPanel.addComponent(channelDataPanel) - val pb = new ProgressBar(0, 100) - pb.setLabelFormat(s"$balance") - pb.setValue((balance.amount * 100 / capacity.amount).toInt) - pb.setPreferredWidth(100) - channelPanel.addComponent(pb) - channelsPanel.addComponent(channelPanel) - channels.put(channel, channelPanel) - } - - def updateState(channel: ActorRef, state: State): Unit = { - val panel = channels(channel) - val channelDataPanel = panel.getChildren.iterator().next().asInstanceOf[Panel] - channelDataPanel.getChildren.toList(1).asInstanceOf[Label].setText(s"$state") - } - - /*val shortcutsPanel = new Panel() - shortcutsPanel.setLayoutManager(new LinearLayout(Direction.HORIZONTAL)) - shortcutsPanel.addComponent(new Label("(N)ew channel")) - shortcutsPanel.addComponent(new Separator(Direction.VERTICAL)) - shortcutsPanel.setLayoutData(BorderLayout.Location.BOTTOM) - mainPanel.addComponent(shortcutsPanel)*/ - - //addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000)) - //addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000)) - //addChannel(randomBytes(32), randomKey.publicKey, NORMAL, Satoshi(Random.nextInt(1000)), Satoshi(1000)) - - //val theme = new SimpleTheme(TextColor.ANSI.DEFAULT, TextColor.ANSI.BLACK) - - // Create window to hold the panel - val window = new BasicWindow - window.setComponent(mainPanel) - //window.setTheme(theme) - window.setHints(/*Window.Hint.FULL_SCREEN :: */ Window.Hint.NO_DECORATIONS :: Nil) - - import scala.concurrent.ExecutionContext.Implicits.global - implicit val timeout = Timeout(30 seconds) - - val textuiUpdater = kit.system.actorOf(SimpleSupervisor.props(Props(classOf[TextuiUpdater], this), "textui-updater", SupervisorStrategy.Resume)) - // Create gui and start gui - val runnable = new Runnable { - override def run(): Unit = { - val gui = new MultiWindowTextGUI(screen, new DefaultWindowManager, new EmptySpace(TextColor.ANSI.BLUE)) - window.addWindowListener(new WindowListener { - override def onMoved(window: Window, terminalPosition: TerminalPosition, terminalPosition1: TerminalPosition): Unit = {} - - override def onResized(window: Window, terminalSize: TerminalSize, terminalSize1: TerminalSize): Unit = {} - - override def onUnhandledInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = {} - - override def onInput(t: Window, keyStroke: KeyStroke, atomicBoolean: AtomicBoolean): Unit = { - if (keyStroke.getCharacter == 'n') { - val input = new TextInputDialogBuilder() - .setTitle("Open a new channel") - .setDescription("Node URI:") - //.setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!") - .build() - .showDialog(gui) - try { - for { - _ <- kit.switchboard ? Peer.Connect(NodeURI.parse(input)) - } yield {} - } catch { - case t: Throwable => logger.error("", t) - } - } else if (keyStroke.getCharacter == 's') { - val input = new TextInputDialogBuilder() - .setTitle("Send a payment") - .setDescription("Payment request:") - //.setValidationPattern(Pattern.compile("[0-9]"), "You didn't enter a single number!") - .build() - .showDialog(gui) - try { - val paymentRequest = PaymentRequest.read(input) - kit.paymentInitiator ! SendPayment(paymentRequest.amount.getOrElse(MilliSatoshi(1000000)).amount, paymentRequest.paymentHash, paymentRequest.nodeId, maxAttempts = 3) - } catch { - case t: Throwable => logger.error("", t) - } - } - } - }) - gui.addWindowAndWait(window) - kit.system.shutdown() - } - } - new Thread(runnable).start() - -} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/TextuiUpdater.scala b/eclair-node/src/main/scala/fr/acinq/eclair/TextuiUpdater.scala deleted file mode 100644 index f48f5d5138..0000000000 --- a/eclair-node/src/main/scala/fr/acinq/eclair/TextuiUpdater.scala +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2018 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 - -import akka.actor.Actor -import fr.acinq.bitcoin.Satoshi -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.payment.PaymentEvent -import fr.acinq.eclair.router.NetworkEvent - -/** - * Created by PM on 31/05/2017. - */ -class TextuiUpdater(textui: Textui) extends Actor { - context.system.eventStream.subscribe(self, classOf[ChannelEvent]) - context.system.eventStream.subscribe(self, classOf[NetworkEvent]) - context.system.eventStream.subscribe(self, classOf[PaymentEvent]) - - override def receive: Receive = { - case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId, _, _) => - textui.addChannel(channel, temporaryChannelId, remoteNodeId, WAIT_FOR_INIT_INTERNAL, Satoshi(0), Satoshi(1)) - - case ChannelRestored(channel, _, remoteNodeId, _, channelId, data) => - textui.addChannel(channel, channelId, remoteNodeId, OFFLINE, Satoshi(33), Satoshi(100)) - - case ChannelStateChanged(channel, _, _, _, state, _) => - textui.updateState(channel, state) - } -} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala new file mode 100644 index 0000000000..3f6533907e --- /dev/null +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -0,0 +1,64 @@ +/* + * 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.api + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import fr.acinq.eclair.payment.PaymentRequest +import FormParamExtractors._ +import JsonSupport.serialization +import JsonSupport.json4sJacksonFormats +import shapeless.HNil +import spray.http.{ContentTypes, HttpEntity, HttpResponse, StatusCodes} +import spray.httpx.marshalling.Marshaller +import spray.routing.directives.OnCompleteFutureMagnet +import spray.routing.{Directive1, Directives, MalformedFormFieldRejection} + +import scala.concurrent.Future +import scala.util.{Failure, Success} +import scala.concurrent.ExecutionContext.Implicits.global + +trait ExtraDirectives extends Directives { + + // named and typed URL parameters used across several routes + val shortChannelIdFormParam_opt = "shortChannelId".as[Option[ShortChannelId]](shortChannelIdUnmarshaller) + val channelIdFormParam_opt = "channelId".as[Option[ByteVector32]](sha256HashUnmarshaller) + val nodeIdFormParam_opt = "nodeId".as[Option[PublicKey]](publicKeyUnmarshaller) + val paymentHashFormParam_opt = "paymentHash".as[Option[ByteVector32]](sha256HashUnmarshaller) + val fromFormParam_opt = "from".as[Long] + val toFormParam_opt = "to".as[Long] + val amountMsatFormParam_opt = "amountMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller) + val invoiceFormParam_opt = "invoice".as[Option[PaymentRequest]](bolt11Unmarshaller) + + // 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: Marshaller[T]) = onComplete(OnCompleteFutureMagnet(fut)) { + case Success(Some(t)) => complete(t) + case Success(None) => + complete(HttpResponse(StatusCodes.NotFound).withEntity(HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("Not found"))))) + case Failure(_) => reject + } + + import shapeless.:: + def withChannelIdentifier: Directive1[Either[ByteVector32, ShortChannelId]] = formFields(channelIdFormParam_opt, shortChannelIdFormParam_opt).hflatMap { + case None :: None :: HNil => reject(MalformedFormFieldRejection("channelId/shortChannelId", "Must specify either the channelId or shortChannelId")) + case Some(channelId) :: None :: HNil => provide(Left(channelId)) + case None :: Some(shortChannelId) :: HNil => provide(Right(shortChannelId)) + case _ => reject(MalformedFormFieldRejection("channelId/shortChannelId", "Must specify either the channelId or shortChannelId")) + } + +} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala new file mode 100644 index 0000000000..bd368d6de7 --- /dev/null +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -0,0 +1,97 @@ +/* + * 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.api + +import java.util.UUID + +import akka.util.Timeout +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.eclair.io.NodeURI +import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} +import scodec.bits.ByteVector +import spray.httpx.unmarshalling.Deserializer +import JsonSupport.json4sJacksonFormats +import JsonSupport.serialization + +import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} + +object FormParamExtractors { + + implicit val publicKeyUnmarshaller: Deserializer[Option[String], Option[PublicKey]] = strictDeserializer { str => + PublicKey(ByteVector.fromValidHex(str)) + } + + implicit val binaryDataUnmarshaller: Deserializer[Option[String], Option[ByteVector]] = strictDeserializer { str => + ByteVector.fromValidHex(str) + } + + implicit val sha256HashUnmarshaller: Deserializer[Option[String], Option[ByteVector32]] = strictDeserializer { bin => + ByteVector32.fromValidHex(bin) + } + + implicit val bolt11Unmarshaller: Deserializer[Option[String], Option[PaymentRequest]] = strictDeserializer { rawRequest => + PaymentRequest.read(rawRequest) + } + + implicit val shortChannelIdUnmarshaller: Deserializer[Option[String], Option[ShortChannelId]] = strictDeserializer { str => + ShortChannelId(str) + } + + implicit val javaUUIDUnmarshaller: Deserializer[Option[String], Option[UUID]] = strictDeserializer { str => + UUID.fromString(str) + } + + implicit val timeoutSecondsUnmarshaller: Deserializer[Option[String], Option[Timeout]] = strictDeserializer { str => + Timeout(str.toInt.seconds) + } + + implicit val nodeURIUnmarshaller: Deserializer[Option[String], Option[NodeURI]] = strictDeserializer { str => + NodeURI.parse(str) + } + + implicit val pubkeyListUnmarshaller: Deserializer[Option[String], Option[List[PublicKey]]] = strictDeserializer { str => + Try(serialization.read[List[String]](str).map { el => + PublicKey(ByteVector.fromValidHex(el), checkValid = false) + }).recoverWith[List[PublicKey]] { + case error => Try(str.split(",").toList.map(pk => PublicKey(ByteVector.fromValidHex(pk)))) + } match { + case Success(list: List[PublicKey]) => list + case Failure(_) => throw new IllegalArgumentException(s"PublicKey list must be either json-encoded or comma separated list") + } + } + + implicit val satoshiUnmarshaller: Deserializer[Option[String], Option[Satoshi]] = strictDeserializer { str => + Satoshi(str.toLong) + } + + implicit val millisatoshiUnmarshaller: Deserializer[Option[String], Option[MilliSatoshi]] = strictDeserializer { str => + MilliSatoshi(str.toLong) + } + + implicit val millisatoshiUnmarshallerOpt: Deserializer[Option[String], Option[MilliSatoshi]] = strictDeserializer { str => + MilliSatoshi(str.toLong) + } + + + def strictDeserializer[T](f: String => T): Deserializer[Option[String], Option[T]] = Deserializer.fromFunction2Converter { + case Some(str) => Some(f(str)) + case None => None + } +} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala new file mode 100644 index 0000000000..e552fecf40 --- /dev/null +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -0,0 +1,282 @@ +/* + * 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.api + +import java.net.InetSocketAddress +import java.util.UUID + +import com.google.common.net.HostAndPort +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.channel.{ChannelVersion, State} +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus} +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.RouteResponse +import fr.acinq.eclair.transactions.Direction +import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} +import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} +import org.json4s.JsonAST._ +import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson} +import scodec.bits.ByteVector +import spray.httpx.{Json4sJacksonSupport, Json4sSupport} + +/** + * JSON Serializers. + * Note: in general, deserialization does not need to be implemented. + */ +class ByteVectorSerializer extends CustomSerializer[ByteVector](_ => ( { + null +}, { + case x: ByteVector => JString(x.toHex) +})) + +class ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { + null +}, { + case x: ByteVector32 => JString(x.toHex) +})) + +class ByteVector64Serializer extends CustomSerializer[ByteVector64](_ => ( { + null +}, { + case x: ByteVector64 => JString(x.toHex) +})) + +class UInt64Serializer extends CustomSerializer[UInt64](_ => ( { + null +}, { + case x: UInt64 => JInt(x.toBigInt) +})) + +class SatoshiSerializer extends CustomSerializer[Satoshi](_ => ( { + null +}, { + case x: Satoshi => JInt(x.toLong) +})) + +class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](_ => ( { + null +}, { + case x: MilliSatoshi => JInt(x.toLong) +})) + +class CltvExpirySerializer extends CustomSerializer[CltvExpiry](_ => ( { + null +}, { + case x: CltvExpiry => JLong(x.toLong) +})) + +class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](_ => ( { + null +}, { + case x: CltvExpiryDelta => JInt(x.toInt) +})) + +class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](_ => ( { + null +}, { + case x: ShortChannelId => JString(x.toString) +})) + +class StateSerializer extends CustomSerializer[State](_ => ( { + null +}, { + case x: State => JString(x.toString) +})) + +class ShaChainSerializer extends CustomSerializer[ShaChain](_ => ( { + null +}, { + case _: ShaChain => JNull +})) + +class PublicKeySerializer extends CustomSerializer[PublicKey](_ => ( { + null +}, { + case x: PublicKey => JString(x.toString()) +})) + +class PrivateKeySerializer extends CustomSerializer[PrivateKey](_ => ( { + null +}, { + case _: PrivateKey => JString("XXX") +})) + +class ChannelVersionSerializer extends CustomSerializer[ChannelVersion](_ => ( { + null +}, { + case x: ChannelVersion => JString(x.bits.toBin) +})) + +class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { + null +}, { + case x: Transaction => JObject(List( + JField("txid", JString(x.txid.toHex)), + JField("tx", JString(x.toString())) + )) +})) + +class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](_ => ( { + null +}, { + case x: TransactionWithInputInfo => JObject(List( + JField("txid", JString(x.tx.txid.toHex)), + JField("tx", JString(x.tx.toString())) + )) +})) + +class InetSocketAddressSerializer extends CustomSerializer[InetSocketAddress](_ => ( { + null +}, { + case address: InetSocketAddress => JString(HostAndPort.fromParts(address.getHostString, address.getPort).toString) +})) + +class OutPointSerializer extends CustomSerializer[OutPoint](_ => ( { + null +}, { + case x: OutPoint => JString(s"${x.txid}:${x.index}") +})) + +class OutPointKeySerializer extends CustomKeySerializer[OutPoint](_ => ( { + null +}, { + case x: OutPoint => s"${x.txid}:${x.index}" +})) + +class InputInfoSerializer extends CustomSerializer[InputInfo](_ => ( { + null +}, { + case x: InputInfo => JObject(("outPoint", JString(s"${x.outPoint.txid}:${x.outPoint.index}")), ("amountSatoshis", JInt(x.txOut.amount.toLong))) +})) + +class ColorSerializer extends CustomSerializer[Color](_ => ( { + null +}, { + case c: Color => JString(c.toString) +})) + +class RouteResponseSerializer extends CustomSerializer[RouteResponse](_ => ( { + null +}, { + case route: RouteResponse => + val nodeIds = route.hops match { + case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId + case Nil => Nil + } + JArray(nodeIds.toList.map(n => JString(n.toString))) +})) + +class ThrowableSerializer extends CustomSerializer[Throwable](_ => ( { + null +}, { + case t: Throwable if t.getMessage != null => JString(t.getMessage) + case t: Throwable => JString(t.getClass.getSimpleName) +})) + +class FailureMessageSerializer extends CustomSerializer[FailureMessage](_ => ( { + null +}, { + case m: FailureMessage => JString(m.message) +})) + +class NodeAddressSerializer extends CustomSerializer[NodeAddress](_ => ( { + null +}, { + case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) +})) + +class DirectionSerializer extends CustomSerializer[Direction](_ => ( { + null +}, { + case d: Direction => JString(d.toString) +})) + +class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( { + null +}, { + 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 fieldList = List(JField("prefix", JString(p.prefix)), + JField("timestamp", JLong(p.timestamp)), + JField("nodeId", JString(p.nodeId.toString())), + JField("serialized", JString(PaymentRequest.write(p))), + JField("description", JString(p.description match { + case Left(l) => l + case Right(r) => r.toString() + })), + JField("paymentHash", JString(p.paymentHash.toString()))) ++ + expiry ++ + minFinalCltvExpiry ++ + amount + JObject(fieldList) +})) + +class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( { + null +}, { + case id: UUID => JString(id.toString) +})) + +object JsonSupport extends Json4sJacksonSupport { + + override val serialization = org.json4s.jackson.Serialization + + override implicit val json4sJacksonFormats = org.json4s.DefaultFormats + + new ByteVectorSerializer + + new ByteVector32Serializer + + new ByteVector64Serializer + + new UInt64Serializer + + new SatoshiSerializer + + new MilliSatoshiSerializer + + new ShortChannelIdSerializer + + new StateSerializer + + new ShaChainSerializer + + new PublicKeySerializer + + new PrivateKeySerializer + + new TransactionSerializer + + new TransactionWithInputInfoSerializer + + new InetSocketAddressSerializer + + new OutPointSerializer + + new OutPointKeySerializer + + new ChannelVersionSerializer + + new InputInfoSerializer + + new ColorSerializer + + new RouteResponseSerializer + + new ThrowableSerializer + + new FailureMessageSerializer + + new NodeAddressSerializer + + new DirectionSerializer + + new PaymentRequestSerializer + + new JavaUUIDSerializer + + case class CustomTypeHints(custom: Map[Class[_], String]) 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 classFor(hint: String): Option[Class[_]] = reverse.get(hint) + } + +} \ No newline at end of file diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala new file mode 100644 index 0000000000..ae720b480e --- /dev/null +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -0,0 +1,237 @@ +/* + * 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.api + +import java.util.UUID + +import akka.actor.{Actor, ActorSystem, Props} +import akka.util.Timeout +import com.google.common.net.HostAndPort +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi} +import fr.acinq.eclair.api.FormParamExtractors._ +import fr.acinq.eclair.io.NodeURI +import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _} +import grizzled.slf4j.Logging +import scodec.bits.ByteVector +import spray.http.CacheDirectives.public +import spray.http.{HttpMethods, StatusCodes} +import spray.http.HttpHeaders._ +import spray.http.CacheDirectives._ +import spray.routing.authentication.{BasicAuth, UserPass} +import spray.routing.{ExceptionHandler, HttpServiceActor, MalformedFormFieldRejection, Route} + +import scala.concurrent.Future +import scala.concurrent.duration._ + +case class ErrorResponse(error: String) + +class Service(password: String, eclairApi: Eclair)(implicit actorSystem: ActorSystem) extends HttpServiceActor with ExtraDirectives with Logging { + + import JsonSupport.{json4sFormats, serialization, json4sMarshaller} + + implicit val ec = actorSystem.dispatcher + + implicit val timeout = Timeout(30 seconds) + + val apiExceptionHandler = ExceptionHandler { + case t: IllegalArgumentException => + logger.error(s"API call failed with cause=${t.getMessage}", t) + complete(StatusCodes.BadRequest, ErrorResponse(t.getMessage)) + case t: Throwable => + logger.error(s"API call failed with cause=${t.getMessage}", t) + complete(StatusCodes.InternalServerError, ErrorResponse(t.getMessage)) + } + + val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: + `Access-Control-Allow-Methods`(HttpMethods.POST :: Nil) :: + `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil + + def userPassAuthenticator(userPass: Option[UserPass]): Future[Option[String]] = userPass match { + case Some(UserPass(user, pass)) if pass == password => Future.successful(Some("user")) + case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force + } + + override def receive: Receive = runRoute(route) + + def route: Route = { + respondWithHeaders(customHeaders) { + handleExceptions(apiExceptionHandler) { + authenticate(BasicAuth(userPassAuthenticator _, realm = "Access restricted")) { _ => + post { + path("getinfo") { + complete(eclairApi.getInfoResponse()) + } ~ + path("connect") { + formFields("uri".as[Option[NodeURI]]) { uri => + complete(eclairApi.connect(Left(uri.get))) + } ~ formFields(nodeIdFormParam_opt, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => + complete(eclairApi.connect(Left(NodeURI(nodeId.get, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))))) + } ~ formFields(nodeIdFormParam_opt) { nodeId => + complete(eclairApi.connect(Right(nodeId.get))) + } + } ~ + path("disconnect") { + formFields(nodeIdFormParam_opt) { nodeId => + complete(eclairApi.disconnect(nodeId.get)) + } + } ~ + path("open") { + formFields(nodeIdFormParam_opt, "fundingSatoshis".as[Option[Satoshi]](satoshiUnmarshaller), "pushMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller), "fundingFeerateSatByte".as[Option[Long]], "channelFlags".as[Option[Int]]) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) => + complete(eclairApi.open(nodeId.get, fundingSatoshis.get, pushMsat, fundingFeerateSatByte, channelFlags, None)) + } + } ~ + path("updaterelayfee") { + withChannelIdentifier { channelIdentifier => + formFields("feeBaseMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller), "feeProportionalMillionths".as[Option[Long]]) { (feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channelIdentifier, feeBase.get, feeProportional.get)) + } + } + } ~ + path("close") { + withChannelIdentifier { channelIdentifier => + formFields("scriptPubKey".as[Option[ByteVector]](binaryDataUnmarshaller)) { scriptPubKey_opt => + complete(eclairApi.close(channelIdentifier, scriptPubKey_opt)) + } + } + } ~ + path("forceclose") { + withChannelIdentifier { channelIdentifier => + complete(eclairApi.forceClose(channelIdentifier)) + } + } ~ + path("peers") { + complete(eclairApi.peersInfo()) + } ~ + path("channels") { + formFields(nodeIdFormParam_opt) { toRemoteNodeId_opt => + complete(eclairApi.channelsInfo(toRemoteNodeId_opt)) + } + } ~ + path("channel") { + withChannelIdentifier { channelIdentifier => + complete(eclairApi.channelInfo(channelIdentifier)) + } + } ~ + path("allnodes") { + complete(eclairApi.allNodes()) + } ~ + path("allchannels") { + complete(eclairApi.allChannels()) + } ~ + path("allupdates") { + formFields(nodeIdFormParam_opt) { nodeId_opt => + complete(eclairApi.allUpdates(nodeId_opt)) + } + } ~ + path("findroute") { + formFields(invoiceFormParam_opt, amountMsatFormParam_opt) { + case (Some(invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _)), None) => complete(eclairApi.findRoute(nodeId, amount, invoice.routingInfo)) + case (Some(invoice), Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) + } + } ~ + path("findroutetonode") { + formFields(nodeIdFormParam_opt, amountMsatFormParam_opt) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId.get, amount.get)) + } + } ~ + path("parseinvoice") { + formFields(invoiceFormParam_opt) { invoice => + complete(invoice) + } + } ~ + path("payinvoice") { + formFields(invoiceFormParam_opt, amountMsatFormParam_opt, "maxAttempts".as[Int].?, "feeThresholdSat".as[Option[Satoshi]](satoshiUnmarshaller), "maxFeePct".as[Double].?, "externalId".?) { + case (Some(invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _)), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, nodeId, amount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case (Some(invoice), Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, invoice.nodeId, overrideAmount, invoice.paymentHash, Some(invoice), maxAttempts, feeThresholdSat_opt, maxFeePct_opt)) + case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) + } + } ~ + path("sendtonode") { + formFields(amountMsatFormParam_opt, paymentHashFormParam_opt, nodeIdFormParam_opt, "maxAttempts".as[Int].?, "feeThresholdSat".as[Option[Satoshi]](satoshiUnmarshaller), "maxFeePct".as[Double].?, "externalId".?) { + (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) => + complete(eclairApi.send(externalId_opt, nodeId.get, amountMsat.get, paymentHash.get, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt)) + } + } ~ + path("sendtoroute") { + formFields(amountMsatFormParam_opt, paymentHashFormParam_opt, "finalCltvExpiry".as[Int], "route".as[Option[List[PublicKey]]](pubkeyListUnmarshaller), "externalId".?) { + (amountMsat, paymentHash, finalCltvExpiry, route, externalId_opt) => + complete(eclairApi.sendToRoute(externalId_opt, route.get, amountMsat.get, paymentHash.get, CltvExpiryDelta(finalCltvExpiry))) + } + } ~ + path("getsentinfo") { + formFields("id".as[Option[UUID]]) { id => + complete(eclairApi.sentInfo(Left(id.get))) + } ~ formFields(paymentHashFormParam_opt) { paymentHash => + complete(eclairApi.sentInfo(Right(paymentHash.get))) + } + } ~ + path("createinvoice") { + formFields("description".as[String], amountMsatFormParam_opt, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[Option[ByteVector32]](sha256HashUnmarshaller)) { + (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt)) + } + } ~ + path("getinvoice") { + formFields(paymentHashFormParam_opt) { paymentHash => + completeOrNotFound(eclairApi.getInvoice(paymentHash.get)) + } + } ~ + path("listinvoices") { + formFields(fromFormParam_opt.?, toFormParam_opt.?) { (from_opt, to_opt) => + complete(eclairApi.allInvoices(from_opt, to_opt)) + } + } ~ + path("listpendinginvoices") { + formFields(fromFormParam_opt.?, toFormParam_opt.?) { (from_opt, to_opt) => + complete(eclairApi.pendingInvoices(from_opt, to_opt)) + } + } ~ + path("getreceivedinfo") { + formFields(paymentHashFormParam_opt) { paymentHash => + completeOrNotFound(eclairApi.receivedInfo(paymentHash.get)) + } ~ formFields(invoiceFormParam_opt) { invoice => + completeOrNotFound(eclairApi.receivedInfo(invoice.get.paymentHash)) + } + } ~ + path("audit") { + formFields(fromFormParam_opt.?, toFormParam_opt.?) { (from_opt, to_opt) => + complete(eclairApi.audit(from_opt, to_opt)) + } + } ~ + path("networkfees") { + formFields(fromFormParam_opt.?, toFormParam_opt.?) { (from_opt, to_opt) => + complete(eclairApi.networkFees(from_opt, to_opt)) + } + } ~ + path("channelstats") { + complete(eclairApi.channelStats()) + } ~ + path("usablebalances") { + complete(eclairApi.usableBalances()) + } + } + } + } + } + } +} diff --git a/eclair-node/src/test/resources/api/close b/eclair-node/src/test/resources/api/close new file mode 100644 index 0000000000..06b1bfe627 --- /dev/null +++ b/eclair-node/src/test/resources/api/close @@ -0,0 +1 @@ +"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0" \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/getinfo b/eclair-node/src/test/resources/api/getinfo new file mode 100644 index 0000000000..1fbb200c3d --- /dev/null +++ b/eclair-node/src/test/resources/api/getinfo @@ -0,0 +1 @@ +{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/help b/eclair-node/src/test/resources/api/help new file mode 100644 index 0000000000..9d58627ae2 --- /dev/null +++ b/eclair-node/src/test/resources/api/help @@ -0,0 +1 @@ +["connect (uri): open a secure connection to a lightning node","connect (nodeId, host, port): open a secure connection to a lightning node","open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced","updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel","peers: list existing local peers","channels: list existing local channels","channels (nodeId): list existing local channels to a particular nodeId","channel (channelId): retrieve detailed information about a given channel","channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)","allnodes: list all known nodes","allchannels: list all known channels","allupdates: list all channels updates","allupdates (nodeId): list all channels updates for this nodeId","receive (amountMsat, description): generate a payment request for a given amount","receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires","parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request","findroute (paymentRequest): returns nodes and channels of the route if there is any","findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any","findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any","send (amountMsat, paymentHash, nodeId): send a payment to a lightning node","send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request","send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount","close (channelId): close a channel","close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey","forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)","checkpayment (paymentHash): returns true if the payment has been received, false otherwise","checkpayment (paymentRequest): returns true if the payment has been received, false otherwise","audit: list all send/received/relayed payments","audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)","networkfees: list all network fees paid to the miners, by transaction","networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)","getinfo: returns info about the blockchain and this node","help: display this message"] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/peers b/eclair-node/src/test/resources/api/peers new file mode 100644 index 0000000000..3e12eddaa8 --- /dev/null +++ b/eclair-node/src/test/resources/api/peers @@ -0,0 +1 @@ +[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"localhost: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 new file mode 100644 index 0000000000..5e8692b1c2 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-expired @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"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 new file mode 100644 index 0000000000..b12ccaf6b3 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-pending @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"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 new file mode 100644 index 0000000000..38a33dc419 --- /dev/null +++ b/eclair-node/src/test/resources/api/received-success @@ -0,0 +1 @@ +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-failed b/eclair-node/src/test/resources/api/sent-failed new file mode 100644 index 0000000000..4e1883146c --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-failed @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"failed","failures":[],"completedAt":2}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-pending b/eclair-node/src/test/resources/api/sent-pending new file mode 100644 index 0000000000..27cab46d7b --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-pending @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"pending"}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/sent-success b/eclair-node/src/test/resources/api/sent-success new file mode 100644 index 0000000000..aeaa2b573e --- /dev/null +++ b/eclair-node/src/test/resources/api/sent-success @@ -0,0 +1 @@ +[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":3}}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/usablebalances b/eclair-node/src/test/resources/api/usablebalances new file mode 100644 index 0000000000..c1ef6b4920 --- /dev/null +++ b/eclair-node/src/test/resources/api/usablebalances @@ -0,0 +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 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 new file mode 100644 index 0000000000..e84d4d3ff0 --- /dev/null +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -0,0 +1,401 @@ +// TODO: port API tests to spray +///* +// * 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.api +// +//import java.util.UUID +// +//import akka.actor.ActorSystem +//import akka.http.scaladsl.model.FormData +//import akka.http.scaladsl.model.StatusCodes._ +//import akka.http.scaladsl.model.headers.BasicHttpCredentials +//import akka.http.scaladsl.server.Route +//import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe} +//import akka.stream.ActorMaterializer +//import akka.util.Timeout +//import de.heikoseeberger.akkahttpjson4s.Json4sSupport +//import fr.acinq.bitcoin.ByteVector32 +//import fr.acinq.bitcoin.Crypto.PublicKey +//import fr.acinq.eclair._ +//import fr.acinq.eclair.io.NodeURI +//import fr.acinq.eclair.io.Peer.PeerInfo +//import fr.acinq.eclair.payment.{PaymentFailed, _} +//import fr.acinq.eclair.wire.NodeAddress +//import org.mockito.scalatest.IdiomaticMockito +//import org.scalatest.{FunSuite, Matchers} +//import scodec.bits._ +// +//import scala.concurrent.Future +//import scala.concurrent.duration._ +//import scala.io.Source +//import scala.reflect.ClassTag +//import scala.util.Try +// +//class ApiServiceSpec extends FunSuite with ScalatestRouteTest with IdiomaticMockito with Matchers { +// +// implicit val formats = JsonSupport.formats +// implicit val serialization = JsonSupport.serialization +// implicit val routeTestTimeout = RouteTestTimeout(3 seconds) +// +// val aliceNodeId = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0") +// val bobNodeId = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585") +// +// class MockService(eclair: Eclair) extends Service { +// override val eclairApi: Eclair = eclair +// +// override def password: String = "mock" +// +// override implicit val actorSystem: ActorSystem = system +// override implicit val mat: ActorMaterializer = materializer +// } +// +// test("API service should handle failures correctly") { +// val mockService = new MockService(mock[Eclair]) +// +// // no auth +// Post("/getinfo") ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == Unauthorized) +// } +// +// // wrong auth +// Post("/getinfo") ~> +// addCredentials(BasicHttpCredentials("", mockService.password + "what!")) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == Unauthorized) +// } +// +// // correct auth but wrong URL +// Post("/mistake") ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == NotFound) +// } +// +// // wrong param type +// Post("/channel", FormData(Map("channelId" -> "hey")).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == BadRequest) +// val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) +// assert(resp.error == "The form field 'channelId' was malformed:\nInvalid hexadecimal character 'h' at index 0") +// } +// +// // wrong params +// Post("/connect", FormData("urb" -> "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735").toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == BadRequest) +// } +// } +// +// test("'peers' should ask the switchboard for current known peers") { +// val eclair = mock[Eclair] +// val mockService = new MockService(eclair) +// eclair.peersInfo()(any[Timeout]) returns Future.successful(List( +// PeerInfo( +// nodeId = aliceNodeId, +// state = "CONNECTED", +// address = Some(NodeAddress.fromParts("localhost", 9731).get.socketAddress), +// channels = 1), +// PeerInfo( +// nodeId = bobNodeId, +// state = "DISCONNECTED", +// address = None, +// channels = 1))) +// +// Post("/peers") ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// val response = entityAs[String] +// eclair.peersInfo()(any[Timeout]).wasCalled(once) +// matchTestJson("peers", response) +// } +// } +// +// test("'usablebalances' asks router for current usable balances") { +// val eclair = mock[Eclair] +// val mockService = new MockService(eclair) +// eclair.usableBalances()(any[Timeout]) returns Future.successful(List( +// UsableBalances(canSend = 100000000 msat, canReceive = 20000000 msat, shortChannelId = ShortChannelId(1), remoteNodeId = aliceNodeId, isPublic = true), +// UsableBalances(canSend = 400000000 msat, canReceive = 30000000 msat, shortChannelId = ShortChannelId(2), remoteNodeId = aliceNodeId, isPublic = false) +// )) +// +// Post("/usablebalances") ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// val response = entityAs[String] +// eclair.usableBalances()(any[Timeout]).wasCalled(once) +// matchTestJson("usablebalances", response) +// } +// } +// +// test("'getinfo' response should include this node ID") { +// val eclair = mock[Eclair] +// val mockService = new MockService(eclair) +// eclair.getInfoResponse()(any[Timeout]) returns Future.successful(GetInfoResponse( +// nodeId = aliceNodeId, +// alias = "alice", +// chainHash = ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), +// blockHeight = 9999, +// publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil +// )) +// +// Post("/getinfo") ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// val resp = entityAs[String] +// assert(resp.toString.contains(aliceNodeId.toString)) +// eclair.getInfoResponse()(any[Timeout]).wasCalled(once) +// matchTestJson("getinfo", resp) +// } +// } +// +// test("'close' method should accept a channelId and shortChannelId") { +// val shortChannelIdSerialized = "42000x27x3" +// val channelId = "56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e" +// +// val eclair = mock[Eclair] +// eclair.close(any, any)(any[Timeout]) returns Future.successful(aliceNodeId.toString()) +// val mockService = new MockService(eclair) +// +// Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// addHeader("Content-Type", "application/json") ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// val resp = entityAs[String] +// assert(resp.contains(aliceNodeId.toString)) +// eclair.close(Right(ShortChannelId(shortChannelIdSerialized)), None)(any[Timeout]).wasCalled(once) +// matchTestJson("close", resp) +// } +// +// Post("/close", FormData("channelId" -> channelId).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// addHeader("Content-Type", "application/json") ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// val resp = entityAs[String] +// assert(resp.contains(aliceNodeId.toString)) +// eclair.close(Left(ByteVector32.fromValidHex(channelId)), None)(any[Timeout]).wasCalled(once) +// matchTestJson("close", resp) +// } +// } +// +// test("'connect' method should accept an URI and a triple with nodeId/host/port") { +// val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") +// val remoteUri = NodeURI.parse("030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735") +// +// val eclair = mock[Eclair] +// eclair.connect(any[Either[NodeURI, PublicKey]])(any[Timeout]) returns Future.successful("connected") +// val mockService = new MockService(eclair) +// +// Post("/connect", FormData("nodeId" -> remoteNodeId.toString()).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// assert(entityAs[String] == "\"connected\"") +// eclair.connect(Right(remoteNodeId))(any[Timeout]).wasCalled(once) +// } +// +// Post("/connect", FormData("uri" -> remoteUri.toString).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// assert(entityAs[String] == "\"connected\"") +// eclair.connect(Left(remoteUri))(any[Timeout]).wasCalled(once) // must account for the previous, identical, invocation +// } +// } +// +// test("'send' method should handle payment failures") { +// val eclair = mock[Eclair] +// eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.failed(new IllegalArgumentException("invoice has expired")) +// val mockService = new MockService(eclair) +// +// val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" +// +// Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == BadRequest) +// val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) +// assert(resp.error == "invoice has expired") +// eclair.send(None, any, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once) +// } +// } +// +// test("'send' method should correctly forward amount parameters to EclairImpl") { +// val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" +// +// val eclair = mock[Eclair] +// eclair.send(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(UUID.randomUUID()) +// val mockService = new MockService(eclair) +// +// Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// eclair.send(None, any, 1258000 msat, any, any, any, any, any)(any[Timeout]).wasCalled(once) +// } +// +// Post("/payinvoice", FormData("invoice" -> invoice, "amountMsat" -> "123", "feeThresholdSat" -> "112233", "maxFeePct" -> "2.34", "externalId" -> "42").toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// eclair.send(Some("42"), any, 123 msat, any, any, any, Some(112233 sat), Some(2.34))(any[Timeout]).wasCalled(once) +// } +// } +// +// test("'getreceivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") { +// val eclair = mock[Eclair] +// eclair.receivedInfo(any[ByteVector32])(any) returns Future.successful(None) +// val mockService = new MockService(eclair) +// +// Post("/getreceivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == NotFound) +// val resp = entityAs[ErrorResponse](Json4sSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) +// assert(resp == ErrorResponse("Not found")) +// eclair.receivedInfo(ByteVector32.Zeroes)(any[Timeout]).wasCalled(once) +// } +// } +// +// test("'sendtoroute' method should accept a both a json-encoded AND comma separaterd list of pubkeys") { +// val rawUUID = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" +// val paymentUUID = UUID.fromString(rawUUID) +// val externalId = UUID.randomUUID().toString +// val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28")) +// val csvNodes = "0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9, 0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3, 026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28" +// val jsonNodes = serialization.write(expectedRoute) +// +// val eclair = mock[Eclair] +// eclair.sendToRoute(any[Option[String]], any[List[PublicKey]], any[MilliSatoshi], any[ByteVector32], any[CltvExpiryDelta])(any[Timeout]) returns Future.successful(paymentUUID) +// val mockService = new MockService(eclair) +// +// Post("/sendtoroute", FormData("route" -> jsonNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.Zeroes.toHex, "finalCltvExpiry" -> "190", "externalId" -> externalId.toString).toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// addHeader("Content-Type", "application/json") ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// assert(entityAs[String] == "\"" + rawUUID + "\"") +// eclair.sendToRoute(Some(externalId), expectedRoute, 1234 msat, ByteVector32.Zeroes, CltvExpiryDelta(190))(any[Timeout]).wasCalled(once) +// } +// +// // this test uses CSV encoded route +// Post("/sendtoroute", FormData("route" -> csvNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.One.toHex, "finalCltvExpiry" -> "190").toEntity) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// addHeader("Content-Type", "application/json") ~> +// Route.seal(mockService.route) ~> +// check { +// assert(handled) +// assert(status == OK) +// assert(entityAs[String] == "\"" + rawUUID + "\"") +// eclair.sendToRoute(None, expectedRoute, 1234 msat, ByteVector32.One, CltvExpiryDelta(190))(any[Timeout]).wasCalled(once) +// } +// } +// +// test("the websocket should return typed objects") { +// val mockService = new MockService(mock[Eclair]) +// val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") +// +// val wsClient = WSProbe() +// +// WS("/ws", wsClient.flow) ~> +// addCredentials(BasicHttpCredentials("", mockService.password)) ~> +// mockService.route ~> +// check { +// +// val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = 1553784963659L) +// val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":1553784963659}""" +// serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf +// system.eventStream.publish(pf) +// wsClient.expectMessage(expectedSerializedPf) +// +// val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, 1553784337711L))) +// val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}]}""" +// serialization.write(ps)(mockService.formatsWithTypeHint) === expectedSerializedPs +// system.eventStream.publish(ps) +// wsClient.expectMessage(expectedSerializedPs) +// +// val prel = PaymentRelayed(amountIn = 21 msat, amountOut = 20 msat, paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L) +// val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" +// serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel +// system.eventStream.publish(prel) +// wsClient.expectMessage(expectedSerializedPrel) +// +// val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, 1553784963659L))) +// val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}]}""" +// serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv +// system.eventStream.publish(precv) +// wsClient.expectMessage(expectedSerializedPrecv) +// +// val pset = PaymentSettlingOnChain(fixedUUID, amount = 21 msat, paymentHash = ByteVector32.One, timestamp = 1553785442676L) +// val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}""" +// serialization.write(pset)(mockService.formatsWithTypeHint) === expectedSerializedPset +// system.eventStream.publish(pset) +// wsClient.expectMessage(expectedSerializedPset) +// } +// +// } +// +// private def matchTestJson(apiName: String, response: String) = { +// val resource = getClass.getResourceAsStream(s"/api/$apiName") +// val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse { +// throw new IllegalArgumentException(s"Mock file for $apiName not found") +// } +// assert(response == expectedResponse, s"Test mock for $apiName did not match the expected response") +// } +// +//} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala new file mode 100644 index 0000000000..5c7aee7b8c --- /dev/null +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -0,0 +1,89 @@ +/* + * 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.api + +import java.net.InetAddress +import java.util.UUID + +import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction} +import fr.acinq.eclair._ +import fr.acinq.eclair.api.JsonSupport.CustomTypeHints +import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} +import fr.acinq.eclair.transactions.{IN, OUT} +import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} +import org.json4s.jackson.Serialization +import org.scalatest.{FunSuite, Matchers} +import scodec.bits._ + +class JsonSerializersSpec extends FunSuite with Matchers { + + test("deserialize Map[OutPoint, ByteVector]") { + val output1 = OutPoint(ByteVector32(hex"11418a2d282a40461966e4f578e1fdf633ad15c1b7fb3e771d14361127233be1"), 0) + val output2 = OutPoint(ByteVector32(hex"3d62bd4f71dc63798418e59efbc7532380c900b5e79db3a5521374b161dd0e33"), 1) + + + val map = Map( + output1 -> hex"dead", + output2 -> hex"beef" + ) + + // it won't work with the default key serializer + val error = intercept[org.json4s.MappingException] { + Serialization.write(map)(org.json4s.DefaultFormats) + } + assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) + + // but it works with our custom key serializer + val json = Serialization.write(map)(org.json4s.DefaultFormats + new ByteVectorSerializer + new OutPointKeySerializer) + assert(json === s"""{"${output1.txid}:0":"dead","${output2.txid}:1":"beef"}""") + } + + 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 tor2 = Tor2("aaaqeayeaudaocaj", 7777) + val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) + + Serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" + Serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" + Serialization.write(tor2)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" + Serialization.write(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" + } + + test("Direction serialization") { + Serialization.write(IN)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""IN"""" + Serialization.write(OUT)(org.json4s.DefaultFormats + new DirectionSerializer) shouldBe s""""OUT"""" + } + + test("Payment Request") { + val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val pr = PaymentRequest.read(ref) + JsonSupport.serialization.write(pr)(JsonSupport.json4sJacksonFormats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000}""" + } + + test("type hints") { + implicit val formats = JsonSupport.json4sJacksonFormats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer + val e1 = PaymentSettlingOnChain(UUID.randomUUID, 42 msat, randomBytes32) + assert(Serialization.writePretty(e1).contains("\"type\" : \"payment-settling-onchain\"")) + } + + test("transaction serializer") { + implicit val formats = JsonSupport.json4sJacksonFormats + val tx = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800") + assert(JsonSupport.serialization.write(tx) == "{\"txid\":\"3ef63b5d297c9dcf93f33b45b9f102733c36e8ef61da1ccf2bc132a10584be18\",\"tx\":\"0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800\"}") + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index f01c0334e3..15c442057a 100644 --- a/pom.xml +++ b/pom.xml @@ -64,10 +64,12 @@ 1.7 2.11.12 2.11 - 2.3.14 + 2.3.14 + 10.0.11 1.3.9 - 0.13 + 0.15 24.0-android + 2.0.0 @@ -125,7 +127,7 @@ -unchecked -Xmax-classfile-name 140 - + -nobootcp @@ -215,12 +217,11 @@ scalatest-maven-plugin 2.0.0 - false + true ${project.build.directory} - -Xmx1024m - -Dfile.encoding=UTF-8 + -Xmx1024m -Dfile.encoding=UTF-8