Compare commits
1 Commits
feature/ho
...
applicatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00894bae64 |
@ -1,6 +0,0 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.git
|
||||
**/*.idea
|
||||
**/*.iml
|
||||
**/target
|
||||
19
.github/ISSUE_TEMPLATE.md
vendored
19
.github/ISSUE_TEMPLATE.md
vendored
@ -1,19 +0,0 @@
|
||||
<!-- This issue tracker is only for technical issues related to Eclair.
|
||||
|
||||
Please do not open issues for support requests or questions about Lightning or Eclair: use our gitter at https://gitter.im/ACINQ/eclair instead.
|
||||
Please check that there is not already a similar issue before opening a new one.
|
||||
-->
|
||||
|
||||
<!-- Describe the issue -->
|
||||
<!-- What behaviour did you expect? -->
|
||||
|
||||
<!-- What was the actual behaviour (provide screenshots if the issue is GUI-related)? -->
|
||||
|
||||
<!-- How reliably can you reproduce the issue, what are the steps to do so? -->
|
||||
|
||||
<!-- What version of Eclair are you using, where did you get it (website, self-compiled, etc)? -->
|
||||
|
||||
<!-- What environment are you observing the error on (OS, JDK version)? -->
|
||||
|
||||
<!-- Any extra information that might be useful in the debugging process. -->
|
||||
<!-- This is normally the contents of a `eclair.log` or `eclair.conf` file. Raw text or a link to a pastebin type site are preferred. -->
|
||||
15
.travis.yml
15
.travis.yml
@ -1,25 +1,14 @@
|
||||
sudo: required
|
||||
services:
|
||||
-docker
|
||||
dist: trusty
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.12
|
||||
- 2.11.11
|
||||
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
|
||||
- export PATH=$M2_HOME/bin:$PATH
|
||||
script:
|
||||
- mvn install
|
||||
cache:
|
||||
directories:
|
||||
- .autoconf
|
||||
- $HOME/.m2
|
||||
jdk:
|
||||
- openjdk11
|
||||
- oraclejdk8
|
||||
notifications:
|
||||
email:
|
||||
- ops@acinq.fr
|
||||
|
||||
36
BUILD.md
36
BUILD.md
@ -1,39 +1,21 @@
|
||||
# Building Eclair
|
||||
|
||||
## Requirements
|
||||
- [OpenJDK 11](https://jdk.java.net/11/).
|
||||
- [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.
|
||||
- [Java Development Kit](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 1.8
|
||||
- [Maven](https://maven.apache.org/download.cgi) 3.3.x
|
||||
- [Inno Setup](http://www.jrsoftware.org/isdl.php) 5.5.9 (optional, if you want to generate the windows installer)
|
||||
|
||||
## Build
|
||||
To build the project, simply run:
|
||||
```shell
|
||||
$ mvn install
|
||||
$ mvn package
|
||||
```
|
||||
|
||||
#### Other build options
|
||||
|
||||
To skip all tests, run:
|
||||
To skip the tests, run:
|
||||
```shell
|
||||
$ mvn install -DskipTests
|
||||
$ mvn package -DskipTests
|
||||
```
|
||||
To only build the `eclair-node` module
|
||||
To generate the windows installer along with the build, run the following command:
|
||||
```shell
|
||||
$ mvn install -pl eclair-node -am -DskipTests
|
||||
$ mvn package -DskipTests -Pinstaller
|
||||
```
|
||||
|
||||
# Building the API documentation
|
||||
|
||||
## 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
|
||||
The generated installer will be located in `eclair-node-gui/target/jfx/installer`
|
||||
|
||||
59
Dockerfile
59
Dockerfile
@ -1,59 +0,0 @@
|
||||
FROM openjdk:8u171-jdk-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 bind-tools
|
||||
|
||||
ARG MAVEN_VERSION=3.6.0
|
||||
ARG USER_HOME_DIR="/root"
|
||||
ARG SHA=6a1b346af36a1f1a491c1c1a141667c5de69b42e6611d3687df26868bc0f4637
|
||||
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
|
||||
|
||||
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
|
||||
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
|
||||
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
|
||||
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
|
||||
&& rm -f /tmp/apache-maven.tar.gz \
|
||||
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
|
||||
|
||||
ENV MAVEN_HOME /usr/share/maven
|
||||
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
|
||||
|
||||
# Let's fetch eclair dependencies, so that Docker can cache them
|
||||
# This way we won't have to fetch dependencies again if only the source code changes
|
||||
# The easiest way to reliably get dependencies is to build the project with no sources
|
||||
WORKDIR /usr/src
|
||||
COPY pom.xml pom.xml
|
||||
COPY eclair-core/pom.xml eclair-core/pom.xml
|
||||
COPY eclair-node/pom.xml eclair-node/pom.xml
|
||||
COPY eclair-node-gui/pom.xml eclair-node-gui/pom.xml
|
||||
RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala
|
||||
# Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core
|
||||
RUN mvn install -pl eclair-node -am
|
||||
RUN mvn clean
|
||||
|
||||
# Only then do we copy the sources
|
||||
COPY . .
|
||||
|
||||
# And this time we can build in offline mode, specifying 'notag' instead of git commit
|
||||
RUN mvn package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.commit.id.abbrev=notag -o
|
||||
# It might be good idea to run the tests here, so that the docker build fail if the code is bugged
|
||||
|
||||
# We currently use a debian image for runtime because of some jni-related issue with sqlite
|
||||
FROM openjdk:8u181-jre-slim
|
||||
RUN apt-get update && apt-get install dnsutils -y
|
||||
WORKDIR /app
|
||||
# Eclair only needs the eclair-node-*.jar to run
|
||||
COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.jar .
|
||||
RUN ln `ls` eclair-node.jar
|
||||
|
||||
ENV ECLAIR_DATADIR=/data
|
||||
ENV JAVA_OPTS=
|
||||
|
||||
RUN mkdir -p "$ECLAIR_DATADIR"
|
||||
VOLUME [ "/data" ]
|
||||
COPY contrib/docker-entrypoint.sh entrypoint.sh
|
||||
|
||||
ENTRYPOINT [ "./entrypoint.sh"]
|
||||
2
LICENSE
2
LICENSE
@ -186,7 +186,7 @@ Apache License
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 ACINQ SAS
|
||||
Copyrigh 2014 ACINQ SAS
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
## JSON-RPC API
|
||||
|
||||
:warning: Note this interface is being deprecated.
|
||||
|
||||
method | params | description
|
||||
------------- |----------------------------------------------------------------------------------------|-----------------------------------------------------------
|
||||
getinfo | | return basic node information (id, chain hash, current block height)
|
||||
connect | nodeId, host, port | open a secure connection to a lightning node
|
||||
connect | uri | 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 opened with 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 | description | generate a payment request without a required amount (can be useful for donations)
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
receive | amountMsat, description, expirySeconds | generate a payment request for a given amount that expires after given number of seconds
|
||||
parseinvoice | paymentRequest | returns node, amount and payment hash in a payment request
|
||||
findroute | paymentRequest | returns nodes and channels of the route for this payment request if there is any
|
||||
findroute | paymentRequest, amountMsat | returns nodes and channels of the route for this payment request and amount, if there is any
|
||||
findroute | nodeId, amountMsat | returns nodes and channels of the route to the nodeId, 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
|
||||
checkpayment | paymentHash | returns true if the payment has been received, false otherwise
|
||||
checkpayment | paymentRequest | returns true if the payment has been received, false otherwise
|
||||
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)"
|
||||
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)
|
||||
help | | display available methods
|
||||
187
README.md
187
README.md
@ -2,21 +2,20 @@
|
||||
|
||||
[](https://travis-ci.org/ACINQ/eclair)
|
||||
[](LICENSE)
|
||||
[](https://gitter.im/ACINQ/eclair)
|
||||
[](https://gitter.im/ACINQ/eclair)
|
||||
|
||||
**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.
|
||||
**Eclair** (french for Lightning) is a scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC 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).
|
||||
This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [lightning-c], [lit], and [lnd].
|
||||
|
||||
---
|
||||
|
||||
:construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change!
|
||||
:construction: Both the BOLTs and Eclair itself are a work in progress. Expect things to break/change!
|
||||
|
||||
:rotating_light: If you intend to run Eclair on mainnet:
|
||||
- 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)
|
||||
- Specific [configuration instructions for mainnet](#mainnet-usage) are provided below (by default Eclair runs on testnet)
|
||||
:warning: Eclair currently only runs on regtest or testnet. We recommend testing in regtest, as it allows you to generate blocks manually and not wait for confirmations.
|
||||
|
||||
:rotating_light: We had reports of Eclair being tested on various segwit-enabled blockchains. Keep in mind that Eclair is still alpha quality software, by using it with actual coins you are putting your funds at risk!
|
||||
|
||||
---
|
||||
|
||||
## Lightning Network Specification Compliance
|
||||
@ -26,49 +25,40 @@ Please see the latest [release note](https://github.com/ACINQ/eclair/releases) f
|
||||
|
||||

|
||||
|
||||
## JSON API
|
||||
|
||||
Eclair offers a feature rich HTTP API that enables application developers to easily integrate.
|
||||
|
||||
For more information please visit the [API documentation website](https://acinq.github.io/eclair).
|
||||
|
||||
:warning: You can still use the old API by setting the `eclair.api.use-old-api=true` parameter, but it is now deprecated and will soon be removed. The old documentation is still available [here](OLD-API-DOCS.md).
|
||||
|
||||
## Installation
|
||||
|
||||
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha5](https://github.com/ACINQ/eclair/blob/v0.2-alpha5/README.md#installation)**.
|
||||
|
||||
### Configuring Bitcoin Core
|
||||
|
||||
:warning: Eclair requires Bitcoin Core 0.16.3 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 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.
|
||||
Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node. This means that on Windows you will need Bitcoin Core 0.14+.
|
||||
|
||||
Run bitcoind with the following minimal `bitcoin.conf`:
|
||||
```
|
||||
testnet=1
|
||||
regtest=1
|
||||
server=1
|
||||
rpcuser=foo
|
||||
rpcpassword=bar
|
||||
rpcuser=XXX
|
||||
rpcpassword=XXX
|
||||
txindex=1
|
||||
zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
addresstype=p2sh-segwit
|
||||
```
|
||||
|
||||
:warning: If you are using Bitcoin Core 0.17.0 you need to add following line to your `bitcoin.conf`:
|
||||
```
|
||||
deprecatedrpc=signrawtransaction
|
||||
```
|
||||
|
||||
### 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
|
||||
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).
|
||||
|
||||
To run Eclair, you first need to install Java, we recommend that you use [OpenJDK 11](https://jdk.java.net/11/). Eclair will also run on Oracle JDK 1.8, Oracle JDK 11, and other versions of OpenJDK but we don't recommend using them.
|
||||
#### Windows
|
||||
|
||||
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:
|
||||
Just use the windows installer, it should create a shortcut on your desktop.
|
||||
|
||||
#### Linux, macOS or manual install on Windows
|
||||
|
||||
You need to first install java, more precisely a [JRE 1.8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html).
|
||||
|
||||
:warning: If you are using the OpenJDK JRE, you will need to build OpenJFX yourself, or run the application in headless mode (see below).
|
||||
|
||||
Then download the latest fat jar and depending on whether or not you want a GUI run the following command:
|
||||
* with GUI:
|
||||
```shell
|
||||
java -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
@ -82,30 +72,25 @@ java -jar eclair-node-<version>-<commit_id>.jar
|
||||
|
||||
#### Configuration file
|
||||
|
||||
Eclair reads its configuration file, and write its logs, to `~/.eclair` by default.
|
||||
Eclair reads its configuration file, and write its logs, to a `datadir` directory, located in `~/.eclair` by default.
|
||||
|
||||
To change your node's configuration, create a file named `eclair.conf` in `~/.eclair`. Here's an example configuration file:
|
||||
To change your node's configuration, create a file named `eclair.conf` in `datadir`. Here's an example configuration file:
|
||||
|
||||
```
|
||||
eclair.chain=testnet
|
||||
eclair.server.port=9735
|
||||
eclair.node-alias=eclair
|
||||
eclair.node-color=49daaa
|
||||
```
|
||||
|
||||
Here are some of the most common options:
|
||||
|
||||
name | description | default value
|
||||
-----------------------------|---------------------------------------------------------------------------------------|--------------
|
||||
eclair.chain | Which blockchain to use: *regtest*, *testnet* or *mainnet* | testnet
|
||||
eclair.server.port | Lightning TCP port | 9735
|
||||
eclair.api.enabled | Enable/disable the API | false. By default the API is disabled. If you want to enable it, you must set a password.
|
||||
eclair.api.port | API HTTP port | 8080
|
||||
eclair.api.password | API password (BASIC) | "" (must be set if the API is enabled)
|
||||
eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo
|
||||
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
|
||||
eclair.bitcoind.zmqblock | Bitcoin Core ZMQ block address | "tcp://127.0.0.1:29000"
|
||||
eclair.bitcoind.zmqtx | Bitcoin Core ZMQ tx address | "tcp://127.0.0.1:29000"
|
||||
eclair.gui.unit | Unit in which amounts are displayed (possible values: msat, sat, bits, mbtc, btc) | btc
|
||||
name | description | default value
|
||||
-----------------------------|---------------------------|--------------
|
||||
eclair.server.port | Lightning TCP port | 9735
|
||||
eclair.api.port | API HTTP port | 8080
|
||||
eclair.bitcoind.rpcuser | Bitcoin Core RPC user | foo
|
||||
eclair.bitcoind.rpcpassword | Bitcoin Core RPC password | bar
|
||||
eclair.bitcoind.zmq | Bitcoin Core ZMQ address | tcp://127.0.0.1:29000
|
||||
|
||||
Quotes are not required unless the value contains special characters. Full syntax guide [here](https://github.com/lightbend/config/blob/master/HOCON.md).
|
||||
|
||||
@ -115,7 +100,7 @@ Quotes are not required unless the value contains special characters. Full synta
|
||||
|
||||
Some advanced parameters can be changed with java environment variables. Most users won't need this and can skip this section.
|
||||
|
||||
:warning: Using separate `datadir` is mandatory if you want to run **several instances of eclair** on the same machine. You will also have to change ports in `eclair.conf` (see above).
|
||||
:warning: Using separate `datadir` is mandatory if you want to run **several instances of eclair** on the same machine. You will also have to change ports in eclair.conf (see above).
|
||||
|
||||
name | description | default value
|
||||
----------------------|--------------------------------------------|--------------
|
||||
@ -128,81 +113,33 @@ For example, to specify a different data directory you would run the following c
|
||||
java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
```
|
||||
|
||||
#### Logging
|
||||
## JSON-RPC API
|
||||
|
||||
Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a different configuration, and override the internal logback.xml, run:
|
||||
|
||||
```shell
|
||||
java -Dlogback.configurationFile=/path/to/logback-custom.xml -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
```
|
||||
|
||||
## 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`.
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair
|
||||
```
|
||||
|
||||
## Mainnet usage
|
||||
|
||||
Following are the minimum configuration files you need to use for Bitcoin Core and Eclair.
|
||||
|
||||
### Bitcoin Core configuration
|
||||
|
||||
```
|
||||
testnet=0
|
||||
server=1
|
||||
rpcuser=<your-rpc-user-here>
|
||||
rpcpassword=<your-rpc-password-here>
|
||||
txindex=1
|
||||
zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
addresstype=p2sh-segwit
|
||||
```
|
||||
|
||||
:warning: If you are using Bitcoin Core 0.17.0 you need to add following line to your `bitcoin.conf`:
|
||||
```
|
||||
deprecatedrpc=signrawtransaction
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
server=1
|
||||
txindex=1
|
||||
addresstype=p2sh-segwit
|
||||
deprecatedrpc=signrawtransaction
|
||||
[main]
|
||||
rpcuser=<your-mainnet-rpc-user-here>
|
||||
rpcpassword=<your-mainnet-rpc-password-here>
|
||||
zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
[test]
|
||||
rpcuser=<your-testnet-rpc-user-here>
|
||||
rpcpassword=<your-testnet-rpc-password-here>
|
||||
zmqpubrawblock=tcp://127.0.0.1:29001
|
||||
zmqpubrawtx=tcp://127.0.0.1:29001
|
||||
```
|
||||
|
||||
### Eclair configuration
|
||||
|
||||
```
|
||||
eclair.chain=mainnet
|
||||
eclair.bitcoind.rpcport=8332
|
||||
eclair.bitcoind.rpcuser=<your-mainnet-rpc-user-here>
|
||||
eclair.bitcoind.rpcpassword=<your-mainnet-rpc-password-here>
|
||||
```
|
||||
method | params | description
|
||||
-------------|-----------------------------------------------|-----------------------------------------------------------
|
||||
getinfo | | return basic node information (id, chain hash, current block height)
|
||||
connect | nodeId, host, port | connect to another lightning node through a secure connection
|
||||
open | nodeId, host, port, fundingSatoshis, pushMsat | opens a channel with another lightning node
|
||||
peers | | list existing local peers
|
||||
channels | | list existing local channels
|
||||
channel | channelId | retrieve detailed information about a given channel
|
||||
allnodes | | list all known nodes
|
||||
allchannels | | list all known channels
|
||||
receive | amountMsat, description | generate a payment request for a given amount
|
||||
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 (optional) | close a channel and send the funds to the given scriptPubKey
|
||||
help | | display available methods
|
||||
|
||||
## 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
|
||||
|
||||
[Amiko-Pay]: https://github.com/cornwarecjp/amiko-pay
|
||||
[lightning-c]: https://github.com/ElementsProject/lightning
|
||||
[lnd]: https://github.com/LightningNetwork/lnd
|
||||
[lit]: https://github.com/mit-dci/lit
|
||||
[Thunder]: https://github.com/blockchain/thunder
|
||||
|
||||
|
||||
135
TOR.md
135
TOR.md
@ -1,135 +0,0 @@
|
||||
## How to Use Tor with Eclair
|
||||
|
||||
### Installing Tor on your node
|
||||
|
||||
#### Linux:
|
||||
|
||||
```shell
|
||||
sudo apt install tor
|
||||
```
|
||||
|
||||
#### Mac OS X:
|
||||
|
||||
```shell
|
||||
brew install tor
|
||||
```
|
||||
|
||||
#### Windows:
|
||||
|
||||
[Download the "Expert Bundle"](https://www.torproject.org/download/download.html) from Tor's website and extract it to `C:\tor`.
|
||||
|
||||
### Configuring Tor
|
||||
|
||||
#### Linux and Max OS X:
|
||||
|
||||
Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled.
|
||||
|
||||
Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X).
|
||||
|
||||
```
|
||||
SOCKSPort 9050
|
||||
ControlPort 9051
|
||||
CookieAuthentication 1
|
||||
ExitPolicy reject *:* # don't change this unless you really know what you are doing
|
||||
```
|
||||
|
||||
Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`).
|
||||
|
||||
#### Windows:
|
||||
|
||||
On Windows it is easier to use the password authentication mechanism.
|
||||
|
||||
First pick a password and hash it with this command:
|
||||
|
||||
```shell
|
||||
$ cd c:\tor\Tor
|
||||
$ tor --hash-password this-is-an-example-password-change-it
|
||||
16:94A50709CAA98333602756426F43E6AC6760B9ADEF217F58219E639E5A
|
||||
```
|
||||
|
||||
Create a Tor configuration file (`C:\tor\Conf\torrc`), edit it and replace the value for `HashedControlPassword` with the result of the command above.
|
||||
|
||||
```
|
||||
SOCKSPort 9050
|
||||
ControlPort 9051
|
||||
HashedControlPassword 16:--REPLACE--THIS--WITH--THE--HASH--OF--YOUR--PASSWORD--
|
||||
ExitPolicy reject *:* # don't change this unless you really know what you are doing
|
||||
```
|
||||
|
||||
### Start Tor
|
||||
|
||||
#### Linux:
|
||||
|
||||
```shell
|
||||
sudo systemctl start tor
|
||||
```
|
||||
|
||||
#### Mac OS X:
|
||||
|
||||
```shell
|
||||
brew services start tor
|
||||
```
|
||||
|
||||
#### Windows:
|
||||
|
||||
Open a CMD with administrator access
|
||||
|
||||
```shell
|
||||
cd c:\tor\Tor
|
||||
tor --service install -options -f c:\tor\Conf\torrc
|
||||
```
|
||||
|
||||
### Configure Tor hidden service
|
||||
|
||||
To create a Tor hidden service endpoint simply set the `eclair.tor.enabled` parameter in `eclair.conf` to true.
|
||||
```
|
||||
eclair.tor.enabled = true
|
||||
```
|
||||
Eclair will automatically set up a hidden service endpoint and add its onion address to the `server.public-ips` list.
|
||||
You can see what onion address is assigned using `eclair-cli`:
|
||||
|
||||
```shell
|
||||
eclair-cli getinfo
|
||||
```
|
||||
Eclair saves the Tor endpoint's private key in `~/.eclair/tor_pk`, so that it can recreate the endpoint address after
|
||||
restart. If you remove the private key eclair will regenerate the endpoint address.
|
||||
|
||||
There are two possible values for `protocol-version`:
|
||||
|
||||
```
|
||||
eclair.tor.protocol-version = "v3"
|
||||
```
|
||||
|
||||
value | description
|
||||
--------|---------------------------------------------------------
|
||||
v2 | set up a Tor hidden service version 2 end point
|
||||
v3 | set up a Tor hidden service version 3 end point (default)
|
||||
|
||||
Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports
|
||||
both v2 and v3 addresses.
|
||||
|
||||
For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`:
|
||||
```
|
||||
eclair.server.binding-ip = "127.0.0.1"
|
||||
```
|
||||
|
||||
### Configure SOCKS5 proxy
|
||||
|
||||
By default all incoming connections will be established via Tor network, but all outgoing will be created via the
|
||||
clearnet. To route them through Tor you can use Tor's SOCKS5 proxy. Add this line in your `eclair.conf`:
|
||||
```
|
||||
eclair.socks5.enabled = true
|
||||
```
|
||||
You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6`
|
||||
or `eclair.socks5.use-for-tor` for fine tuning.
|
||||
|
||||
To create a new Tor circuit for every connection, use `randomize-credentials` parameter:
|
||||
|
||||
```
|
||||
eclair.socks5.randomize-credentials = true
|
||||
```
|
||||
|
||||
:warning: Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get the most privacy
|
||||
features from using Tor use both.
|
||||
|
||||
Note, that bitcoind should be configured to use Tor as well (https://en.bitcoin.it/wiki/Setting_up_a_Tor_hidden_service).
|
||||
@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
ECLAIR_RESOLVED_IP_OPT=""
|
||||
if ! [ -z "$PUBLIC_HOST" ]; then
|
||||
RESOLVED_IP="$(dig +short $PUBLIC_HOST | grep -Eo '[0-9\.]{7,15}' | head -1)"
|
||||
if ! [ -z "$RESOLVED_IP" ]; then
|
||||
ECLAIR_RESOLVED_IP_OPT=" -Declair.server.public-ips.0=$RESOLVED_IP"
|
||||
else
|
||||
ECLAIR_RESOLVED_IP_OPT=""
|
||||
fi
|
||||
fi
|
||||
|
||||
java $JAVA_OPTS $ECLAIR_RESOLVED_IP_OPT -Declair.datadir=$ECLAIR_DATADIR -jar eclair-node.jar
|
||||
@ -1,39 +0,0 @@
|
||||
# bash completion for eclair-cli
|
||||
# copy to /etc/bash_completion.d/
|
||||
# created by Stadicus
|
||||
|
||||
_eclair-cli()
|
||||
{
|
||||
local cur prev opts cmds
|
||||
|
||||
# eclair-cli might not be in $PATH
|
||||
local eclaircli
|
||||
ecli="$1"
|
||||
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
case "$cur" in
|
||||
-*=*)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
# works fine, but is too slow at the moment.
|
||||
# allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g')
|
||||
allopts="getinfo connect open close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates receive parseinvoice findroute findroutetonode send sendtonode checkpayment audit networkfees channelstats"
|
||||
|
||||
if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments
|
||||
if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then
|
||||
opts=${allopts}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$cur" || "$cur" =~ ^- ]]; then
|
||||
cmds=$($ecli 2>&1 | awk '$1 ~ /^-/ { sub(/,/, ""); print $1}')
|
||||
fi
|
||||
|
||||
COMPREPLY=( $(compgen -W "${cmds} ${opts}" -- ${cur}) )
|
||||
esac
|
||||
}
|
||||
complete -F _eclair-cli eclair-cli
|
||||
143
eclair-core/eclair-cli
Executable file → Normal file
143
eclair-core/eclair-cli
Executable file → Normal file
@ -1,109 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
# default script values, can be overriden for convenience.
|
||||
api_url='http://localhost:8080'
|
||||
# uncomment the line below if you don't want to provide a password each time you call eclair-cli
|
||||
# api_password='your_api_password'
|
||||
# for some commands the json output can be shortened for better readability
|
||||
short=false
|
||||
[ -z "$1" ] && (
|
||||
echo "usage: "
|
||||
echo " eclair-cli help"
|
||||
) && exit 1
|
||||
|
||||
# prints help message
|
||||
usage() {
|
||||
echo -e "==============================
|
||||
Command line client for eclair
|
||||
==============================
|
||||
URL="http://localhost:8080"
|
||||
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\""
|
||||
|
||||
This tool requires the eclair node's API to be enabled and listening
|
||||
on <$api_url>.
|
||||
|
||||
Usage
|
||||
-----
|
||||
\e[93meclair-cli\e[39m [\e[93mOPTIONS\e[39m]... <\e[93mCOMMAND\e[39m> [--command-param=command-value]...
|
||||
|
||||
where OPTIONS can be:
|
||||
-p <password> API's password
|
||||
-a <address> Override the API URL with <address>
|
||||
-h Show this help
|
||||
-s Some commands can print a trimmed JSON
|
||||
|
||||
and COMMAND is one of:
|
||||
getinfo, connect, open, close, forceclose, updaterelayfee,
|
||||
peers, channels, channel, allnodes, allchannels, allupdates,
|
||||
receive, parseinvoice, findroute, findroutetonode,
|
||||
payinvoice, sendtonode, getreceivedinfo, audit, networkfees,
|
||||
channelstats, getsentinfo, getinvoice, allinvoice, listpendinginvoices
|
||||
|
||||
Examples
|
||||
--------
|
||||
eclair-cli -a localhost:1234 peers list the peers of a node hosted on localhost:1234
|
||||
eclair-cli close --channelId=006fb... closes the channel with id 006fb...
|
||||
|
||||
|
||||
Full documentation here: <https://acinq.github.io/eclair>" 1>&2;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
# -- script's logic begins here
|
||||
|
||||
# Check if jq is installed. If not, display instructions and abort program
|
||||
command -v jq >/dev/null 2>&1 || { echo -e "This tool requires jq.\nFor installation instructions, visit https://stedolan.github.io/jq/download/.\n\nAborting..."; exit 1; }
|
||||
|
||||
# curl installed? If not, give a hint
|
||||
command -v curl >/dev/null 2>&1 || { echo -e "This tool requires curl.\n\nAborting..."; exit 1; }
|
||||
|
||||
# extract script options
|
||||
while getopts ':cu:su:p:a:hu:' flag; do
|
||||
case "${flag}" in
|
||||
p) api_password="${OPTARG}" ;;
|
||||
a) api_url="${OPTARG}" ;;
|
||||
h) usage ;;
|
||||
s) short=true ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
shift $(($OPTIND - 1))
|
||||
|
||||
# extract api's endpoint (e.g. sendpayment, connect, ...) from params
|
||||
api_endpoint=${1}
|
||||
shift 1
|
||||
|
||||
# display a usage method if no method given or help requested
|
||||
if [ -z $api_endpoint ] || [ "$api_endpoint" == "help" ]; then
|
||||
usage;
|
||||
fi
|
||||
|
||||
# long options are expected to be of format: --param=param_value
|
||||
api_payload=""
|
||||
for arg in "${@}"; do
|
||||
case ${arg} in
|
||||
"--"*) api_payload="$api_payload --data-urlencode \"${arg:2}\"";
|
||||
;;
|
||||
*) echo "incorrect argument, please use --arg=value"; usage;
|
||||
;;
|
||||
esac
|
||||
done;
|
||||
|
||||
# jq filter parses response body for error message
|
||||
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 }";
|
||||
case $api_endpoint in
|
||||
"channels") jq_filter="$jq_filter | map( $jq_channel_filter )" ;;
|
||||
"channel") jq_filter="$jq_filter | $jq_channel_filter" ;;
|
||||
*) ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
jq_filter="$jq_filter end";
|
||||
|
||||
# if no password is provided, auth should only contain user login so that curl prompts for the api password
|
||||
if [ -z $api_password ]; then
|
||||
auth="eclair-cli";
|
||||
else
|
||||
auth="eclair-cli:$api_password";
|
||||
fi
|
||||
|
||||
# we're now ready to execute the API call
|
||||
eval curl "--user $auth --silent --show-error -X POST -H \"Content-Type: application/x-www-form-urlencoded\" $api_payload $api_url/$api_endpoint" | jq -r "$jq_filter"
|
||||
case $1 in
|
||||
"help")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"help\", \"params\" : [] }' $URL" | jq -r ".result[]"
|
||||
;;
|
||||
"getinfo")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"getinfo\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"channels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
|
||||
;;
|
||||
"channel")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
|
||||
;;
|
||||
"open")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing node id"}\", \"${3?"missing ip"}\", ${4?"missing port"}, ${5?"missing amount (sat)"}, ${6?"missing push amount (msat)"}] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"close")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
|
||||
;;
|
||||
"receive")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"send")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"network")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"peers")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -1,27 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.3-SNAPSHOT</version>
|
||||
<version>0.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-core_2.11</artifactId>
|
||||
@ -31,6 +15,17 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>pl.project13.maven</groupId>
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.googlecode.maven-download-plugin</groupId>
|
||||
<artifactId>download-maven-plugin</artifactId>
|
||||
@ -79,10 +74,10 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-x86_64-linux-gnu.tar.gz
|
||||
</bitcoind.url>
|
||||
<bitcoind.md5>724043999e2b5ed0c088e8db34f15d43</bitcoind.md5>
|
||||
<bitcoind.sha1>546ee35d4089c7ccc040a01cdff3362599b8bc53</bitcoind.sha1>
|
||||
<bitcoind.md5>c811c157d4d618f7d7f4b9f24834551c</bitcoind.md5>
|
||||
<bitcoind.sha1>3ab7e537bd00bf35e6a78fca108d0d886f8289c1</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
@ -93,10 +88,10 @@
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-osx64.tar.gz
|
||||
</bitcoind.url>
|
||||
<bitcoind.md5>b5a792c6142995faa42b768273a493bd</bitcoind.md5>
|
||||
<bitcoind.sha1>8bd51c7024d71de07df381055993e9f472013db8</bitcoind.sha1>
|
||||
<bitcoind.md5>1521e1d0901169004b9c1c9b552868b7</bitcoind.md5>
|
||||
<bitcoind.sha1>7216298f77162618f322fdf499f1f1b67a0048b7</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
@ -107,9 +102,9 @@
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip</bitcoind.url>
|
||||
<bitcoind.md5>b0e824e9dd02580b5b01f073f3c89858</bitcoind.md5>
|
||||
<bitcoind.sha1>4e17bad7d08c465b444143a93cd6eb1c95076e3f</bitcoind.sha1>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-win64.zip</bitcoind.url>
|
||||
<bitcoind.md5>e84bc3a81ad3d1776299419eb7a04935</bitcoind.md5>
|
||||
<bitcoind.sha1>d2e64fcabf6f85d56d64a52c76e007b6defc32ef</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
@ -126,39 +121,21 @@
|
||||
<artifactId>akka-slf4j_${scala.version.short}</artifactId>
|
||||
<version>${akka.version}</version>
|
||||
</dependency>
|
||||
<!-- HTTP SERVER -->
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-http-core_${scala.version.short}</artifactId>
|
||||
<version>${akka.http.version}</version>
|
||||
</dependency>
|
||||
<!-- HTTP CLIENT -->
|
||||
<dependency>
|
||||
<groupId>com.softwaremill.sttp</groupId>
|
||||
<artifactId>okhttp-backend_${scala.version.short}</artifactId>
|
||||
<version>${sttp.version}</version>
|
||||
<version>10.0.7</version>
|
||||
</dependency>
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>org.json4s</groupId>
|
||||
<artifactId>json4s-jackson_${scala.version.short}</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.5.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.heikoseeberger</groupId>
|
||||
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
|
||||
<version>1.19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.softwaremill.sttp</groupId>
|
||||
<artifactId>json4s_${scala.version.short}</artifactId>
|
||||
<version>${sttp.version}</version>
|
||||
</dependency>
|
||||
<!-- TCP -->
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.1.32.Final</version>
|
||||
<version>1.16.1</version>
|
||||
</dependency>
|
||||
<!-- BITCOIN -->
|
||||
<dependency>
|
||||
@ -175,18 +152,18 @@
|
||||
<dependency>
|
||||
<groupId>org.zeromq</groupId>
|
||||
<artifactId>jeromq</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.acinq</groupId>
|
||||
<artifactId>bitcoinj-core</artifactId>
|
||||
<version>${bitcoinj.version}</version>
|
||||
</dependency>
|
||||
<!-- SERIALIZATION -->
|
||||
<dependency>
|
||||
<groupId>org.scodec</groupId>
|
||||
<artifactId>scodec-core_${scala.version.short}</artifactId>
|
||||
<version>1.11.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.9</version>
|
||||
<version>1.10.3</version>
|
||||
</dependency>
|
||||
<!-- LOGGING -->
|
||||
<dependency>
|
||||
@ -195,15 +172,26 @@
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
<!-- OTHER -->
|
||||
<dependency>
|
||||
<groupId>org.jheaps</groupId>
|
||||
<artifactId>jheaps</artifactId>
|
||||
<version>0.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.21.0.1</version>
|
||||
<version>3.20.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-core</artifactId>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-ext</artifactId>
|
||||
<version>1.0.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.tinyjee.jgraphx</groupId>
|
||||
<artifactId>jgraphx</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- This is to get rid of '[WARNING] warning: Class javax.annotation.Nonnull not found - continuing with a stub.' compile errors -->
|
||||
@ -211,37 +199,7 @@
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<!-- TESTS -->
|
||||
<dependency>
|
||||
<groupId>com.softwaremill.quicklens</groupId>
|
||||
<artifactId>quicklens_${scala.version.short}</artifactId>
|
||||
<version>1.4.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.whisk</groupId>
|
||||
<artifactId>docker-testkit-scalatest_${scala.version.short}</artifactId>
|
||||
<version>0.9.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.whisk</groupId>
|
||||
<artifactId>docker-testkit-impl-spotify_${scala.version.short}</artifactId>
|
||||
<version>0.9.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- neeeded for our docker tests, see https://github.com/spotify/dockerfile-maven/issues/90 -->
|
||||
<dependency>
|
||||
<groupId>javax.activation</groupId>
|
||||
<artifactId>activation</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-testkit_${scala.version.short}</artifactId>
|
||||
@ -254,11 +212,5 @@
|
||||
<version>1.2.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-http-testkit_${scala.version.short}</artifactId>
|
||||
<version>${akka.http.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,400 +0,0 @@
|
||||
{
|
||||
"3smoooajg7qqac2y.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"81-7-10-251.blue.kundencontroller.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"E-X.not.fyi": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"MEADS.hopto.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"VPS.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"b.ooze.cc": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bauerjda5hnedjam.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bauerjhejlv6di7s.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bitcoin.corgi.party": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bitcoin3nqy3db7c.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bitcoins.sk": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.cihar.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.smsys.me": {
|
||||
"pruning": "-",
|
||||
"s": "995",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.xskyx.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"cashyes.zapto.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"currentlane.lovebitco.in": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"daedalus.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"dedi.jochen-hoenicke.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"dragon085.startdedicated.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"e-1.claudioboxx.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"e.keff.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"elec.luggs.co": {
|
||||
"pruning": "-",
|
||||
"s": "443",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum-server.ninja": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum-unlimited.criptolayer.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.eff.ro": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.festivaldelhumor.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.leblancnet.us": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.mindspot.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.qtornado.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.taborsky.cz": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.villocq.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum2.eff.ro": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum2.villocq.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.bot.nu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.ddns.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.ftp.sh": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.ml": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.nmdps.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.soon.it": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumxhqdsmlu.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"elx01.knas.systems": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"enode.duckdns.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"erbium1.sytes.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"fedaykin.goip.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"fn.48.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50003",
|
||||
"version": "1.4"
|
||||
},
|
||||
"helicarrier.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"hsmiths4fyqlw5xw.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"hsmiths5mjk6uijs.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"icarus.tetradrachm.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"kirsche.emzy.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"luggscoqbymhvnkp.onion": {
|
||||
"pruning": "-",
|
||||
"t": "80",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ndnd.selfhost.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ndndword5lpb7eex.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"orannis.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ozahtqwp25chjdjd.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"qtornadoklbgdyww.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"rbx.curalle.ovh": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"s7clinmo4cazmhul.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"tardis.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"technetium.network": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"tomscryptos.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ulrichard.ch": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"us.electrum.be": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"vmd27610.contaboserver.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"vmd30612.contaboserver.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"xray587.startdedicated.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"yuio.top": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,14 @@
|
||||
{
|
||||
"electrumx.kekku.li": {
|
||||
"pruning": "-",
|
||||
"s": "51002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"hsmithsxurybd7uh.onion": {
|
||||
"pruning": "-",
|
||||
"s": "53012",
|
||||
"t": "53011",
|
||||
"version": "1.2"
|
||||
},
|
||||
"testnet.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "53012",
|
||||
"t": "53011",
|
||||
"version": "1.2"
|
||||
},
|
||||
"testnet.qtornado.com": {
|
||||
"pruning": "-",
|
||||
"s": "51002",
|
||||
"t": "51001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"testnet1.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
}
|
||||
"testnetnode.arihanc.com": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
},
|
||||
"testnet.hsmiths.com": {
|
||||
"t": "53011",
|
||||
"s": "53012"
|
||||
},
|
||||
"electrum.akinbo.org": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
eclair {
|
||||
|
||||
chain = "testnet" // "regtest" for regtest, "testnet" for testnet, "mainnet" for mainnet
|
||||
chain = "test" // "regtest" for regtest, "test" for testnet. Livenet is not supported.
|
||||
|
||||
server {
|
||||
public-ips = [] // external ips, will be announced on the network
|
||||
@ -9,66 +9,61 @@ eclair {
|
||||
}
|
||||
|
||||
api {
|
||||
enabled = false // disabled by default for security reasons
|
||||
binding-ip = "127.0.0.1"
|
||||
port = 8080
|
||||
password = "" // password for basic auth, must be non empty if json-rpc api is enabled
|
||||
use-old-api = false
|
||||
}
|
||||
|
||||
watcher-type = "bitcoind" // other *experimental* values include "electrum"
|
||||
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
|
||||
|
||||
bitcoind {
|
||||
host = "localhost"
|
||||
rpcport = 18332
|
||||
rpcuser = "foo"
|
||||
rpcpassword = "bar"
|
||||
zmqblock = "tcp://127.0.0.1:29000"
|
||||
zmqtx = "tcp://127.0.0.1:29000"
|
||||
zmq = "tcp://127.0.0.1:29000"
|
||||
}
|
||||
|
||||
default-feerates { // those are in satoshis per kilobyte
|
||||
bitcoinj {
|
||||
static-peers = [
|
||||
#{ // currently used in integration tests to override default port
|
||||
# host = "localhost"
|
||||
# port = 28333
|
||||
#}
|
||||
]
|
||||
}
|
||||
|
||||
default-feerates { // those are in satoshis per byte
|
||||
delay-blocks {
|
||||
1 = 210000
|
||||
2 = 180000
|
||||
6 = 150000
|
||||
12 = 110000
|
||||
36 = 50000
|
||||
72 = 20000
|
||||
1 = 210
|
||||
2 = 180
|
||||
6 = 150
|
||||
12 = 110
|
||||
36 = 50
|
||||
72 = 20
|
||||
}
|
||||
}
|
||||
min-feerate = 2 // minimum feerate in satoshis per byte
|
||||
smooth-feerate-window = 3 // 1 = no smoothing
|
||||
|
||||
node-alias = "eclair"
|
||||
node-color = "49daaa"
|
||||
|
||||
global-features = ""
|
||||
local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries
|
||||
override-features = [ // optional per-node features
|
||||
# {
|
||||
# nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
# global-features = "",
|
||||
# local-features = ""
|
||||
# }
|
||||
]
|
||||
local-features = "08" // initial_routing_sync
|
||||
channel-flags = 1 // announce channels
|
||||
dust-limit-satoshis = 542
|
||||
default-feerate-per-kb = 20000 // default bitcoin core value
|
||||
|
||||
dust-limit-satoshis = 546
|
||||
max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC
|
||||
htlc-minimum-msat = 1
|
||||
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
|
||||
htlc-minimum-msat = 1000000
|
||||
max-accepted-htlcs = 30
|
||||
|
||||
reserve-to-funding-ratio = 0.01 // recommended by BOLT #2
|
||||
max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%)
|
||||
|
||||
to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days)
|
||||
max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks)
|
||||
mindepth-blocks = 3
|
||||
delay-blocks = 144
|
||||
mindepth-blocks = 2
|
||||
expiry-delta-blocks = 144
|
||||
|
||||
fee-base-msat = 1000
|
||||
fee-proportional-millionths = 100 // fee charged per transferred satoshi in millionths of a satoshi (100 = 0.01%)
|
||||
fee-base-msat = 546000
|
||||
fee-proportional-millionth = 10
|
||||
|
||||
// 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
|
||||
@ -78,59 +73,12 @@ eclair {
|
||||
// than this ratio.
|
||||
update-fee_min-diff-ratio = 0.1
|
||||
|
||||
revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting
|
||||
channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration
|
||||
router-broadcast-interval = 10 seconds // this should be 60 seconds on mainnet
|
||||
router-validate-interval = 2 seconds // this should be high enough to have a decent level of parallelism
|
||||
|
||||
ping-interval = 30 seconds
|
||||
ping-timeout = 10 seconds // will disconnect if peer takes longer than that to respond
|
||||
ping-disconnect = true // disconnect if no answer to our pings
|
||||
auto-reconnect = true
|
||||
|
||||
payment-handler = "local"
|
||||
payment-request-expiry = 1 hour // default expiry for payment requests generated by this node
|
||||
min-funding-satoshis = 100000
|
||||
max-payment-attempts = 5
|
||||
|
||||
autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels
|
||||
|
||||
router {
|
||||
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
|
||||
init-timeout = 5 minutes
|
||||
|
||||
// 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
|
||||
max-cltv = 1008 // max acceptable cltv expiry for the payment (1008 ~ 1 week)
|
||||
fee-threshold-sat = 21 // if fee is below this value we skip the max-fee-pct check
|
||||
max-fee-pct = 0.03 // route will be discarded if fee is above this value (in percentage relative to the total payment amount); doesn't apply if fee < fee-threshold-sat
|
||||
|
||||
// channel 'weight' is computed with the following formula: channelFee * (cltvDelta * ratio-cltv + channelAge * ratio-channel-age + channelCapacity * ratio-channel-capacity)
|
||||
// the following parameters can be used to ask the router to use heuristics to find i.e: 'cltv-optimized' routes, **the sum of the three ratios must be > 0 and <= 1**
|
||||
heuristics-enable = true // if true uses heuristics for path-finding
|
||||
ratio-cltv = 0.15 // when computing the weight for a channel, consider its CLTV delta in this proportion
|
||||
ratio-channel-age = 0.35 // when computing the weight for a channel, consider its AGE in this proportion
|
||||
ratio-channel-capacity = 0.5 // when computing the weight for a channel, consider its CAPACITY in this proportion
|
||||
}
|
||||
}
|
||||
|
||||
socks5 {
|
||||
enabled = false
|
||||
host = "127.0.0.1"
|
||||
port = 9050
|
||||
use-for-ipv4 = true
|
||||
use-for-ipv6 = true
|
||||
use-for-tor = true
|
||||
randomize-credentials = false // this allows tor stream isolation
|
||||
}
|
||||
|
||||
tor {
|
||||
enabled = false
|
||||
protocol = "v3" // v2, v3
|
||||
auth = "password" // safecookie, password
|
||||
password = "foobar" // used when auth=password
|
||||
host = "127.0.0.1"
|
||||
port = 9051
|
||||
private-key-file = "tor.dat"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,243 +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.text.{DecimalFormat, NumberFormat}
|
||||
|
||||
import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, MilliSatoshi, 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.
|
||||
*/
|
||||
private sealed trait BtcAmountGUILossless {
|
||||
def amount_msat: Long
|
||||
def unit: CoinUnit
|
||||
def toMilliSatoshi: MilliSatoshi = MilliSatoshi(amount_msat)
|
||||
}
|
||||
|
||||
private case class GUIMSat(amount_msat: Long) extends BtcAmountGUILossless {
|
||||
override def unit: CoinUnit = MSatUnit
|
||||
}
|
||||
private case class GUISat(amount_msat: Long) extends BtcAmountGUILossless {
|
||||
override def unit: CoinUnit = SatUnit
|
||||
}
|
||||
private case class GUIBits(amount_msat: Long) extends BtcAmountGUILossless {
|
||||
override def unit: CoinUnit = BitUnit
|
||||
}
|
||||
private case class GUIMBtc(amount_msat: Long) extends BtcAmountGUILossless {
|
||||
override def unit: CoinUnit = MBtcUnit
|
||||
}
|
||||
private case class GUIBtc(amount_msat: Long) extends BtcAmountGUILossless {
|
||||
override def unit: CoinUnit = BtcUnit
|
||||
}
|
||||
|
||||
sealed trait CoinUnit {
|
||||
def code: String
|
||||
def shortLabel: String
|
||||
def label: String
|
||||
def factorToMsat: Long
|
||||
}
|
||||
|
||||
case object MSatUnit extends CoinUnit {
|
||||
override def code: String = "msat"
|
||||
override def shortLabel: String = "mSat"
|
||||
override def label: String = "MilliSatoshi"
|
||||
override def factorToMsat: Long = 1L
|
||||
}
|
||||
|
||||
case object SatUnit extends CoinUnit {
|
||||
override def code: String = "sat"
|
||||
override def shortLabel: String = "sat"
|
||||
override def label: String = "Satoshi"
|
||||
override def factorToMsat: Long = 1000L // 1 sat = 1 000 msat
|
||||
}
|
||||
|
||||
case object BitUnit extends CoinUnit {
|
||||
override def code: String = "bits"
|
||||
override def shortLabel: String = "bits"
|
||||
override def label: String = "Bits"
|
||||
override def factorToMsat: Long = 100 * 1000L // 1 bit = 100 sat = 100 000 msat
|
||||
}
|
||||
|
||||
case object MBtcUnit extends CoinUnit {
|
||||
override def code: String = "mbtc"
|
||||
override def shortLabel: String = "mBTC"
|
||||
override def label: String = "MilliBitcoin"
|
||||
override def factorToMsat: Long = 1000 * 100000L // 1 mbtc = 1 00000 000 msat
|
||||
}
|
||||
|
||||
case object BtcUnit extends CoinUnit {
|
||||
override def code: String = "btc"
|
||||
override def shortLabel: String = "BTC"
|
||||
override def label: String = "Bitcoin"
|
||||
override def factorToMsat: Long = 1000 * 100000 * 1000L // 1 btc = 1 000 00000 000 msat
|
||||
}
|
||||
|
||||
object CoinUtils extends Logging {
|
||||
|
||||
// msat pattern, no decimals allowed
|
||||
val MILLI_SAT_PATTERN = "#,###,###,###,###,###,##0"
|
||||
|
||||
// sat pattern decimals are optional
|
||||
val SAT_PATTERN = "#,###,###,###,###,##0.###"
|
||||
|
||||
// bits pattern always shows 2 decimals (msat optional)
|
||||
val BITS_PATTERN = "##,###,###,###,##0.00###"
|
||||
|
||||
// milli btc pattern always shows 5 decimals (msat optional)
|
||||
val MILLI_BTC_PATTERN = "##,###,###,##0.00000###"
|
||||
|
||||
// btc pattern always shows 8 decimals (msat optional). This is the default pattern.
|
||||
val BTC_PATTERN = "##,###,##0.00000000###"
|
||||
|
||||
var COIN_FORMAT: NumberFormat = new DecimalFormat(BTC_PATTERN)
|
||||
|
||||
def setCoinPattern(pattern: String): Unit = {
|
||||
COIN_FORMAT = new DecimalFormat(pattern)
|
||||
}
|
||||
|
||||
def getPatternFromUnit(unit: CoinUnit): String = {
|
||||
unit match {
|
||||
case MSatUnit => MILLI_SAT_PATTERN
|
||||
case SatUnit => SAT_PATTERN
|
||||
case BitUnit => BITS_PATTERN
|
||||
case MBtcUnit => MILLI_BTC_PATTERN
|
||||
case BtcUnit => BTC_PATTERN
|
||||
case _ => throw new IllegalArgumentException("unhandled unit")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
val amountDecimal = BigDecimal(amount)
|
||||
if (amountDecimal < 0) {
|
||||
throw new IllegalArgumentException("amount must be equal or greater than 0")
|
||||
}
|
||||
// note: we can't use the fr.acinq.bitcoin._ conversion methods because they truncate the sub-satoshi part
|
||||
getUnitFromString(unit) match {
|
||||
case MSatUnit => MilliSatoshi((amountDecimal * MSatUnit.factorToMsat).longValue())
|
||||
case SatUnit => MilliSatoshi((amountDecimal * SatUnit.factorToMsat).longValue())
|
||||
case BitUnit => MilliSatoshi((amountDecimal * BitUnit.factorToMsat).longValue())
|
||||
case MBtcUnit => MilliSatoshi((amountDecimal * MBtcUnit.factorToMsat).longValue())
|
||||
case BtcUnit => MilliSatoshi((amountDecimal * BtcUnit.factorToMsat).longValue())
|
||||
case _ => throw new IllegalArgumentException("unhandled unit")
|
||||
}
|
||||
}
|
||||
|
||||
def convertStringAmountToSat(amount: String, unit: String): Satoshi =
|
||||
fr.acinq.bitcoin.millisatoshi2satoshi(CoinUtils.convertStringAmountToMsat(amount, unit))
|
||||
|
||||
/**
|
||||
* Only BtcUnit, MBtcUnit, BitUnit, SatUnit and MSatUnit codes or label are supported.
|
||||
* @param unit
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
case u if u == BitUnit.code || u == BitUnit.label.toLowerCase() => BitUnit
|
||||
case u if u == MBtcUnit.code || u == MBtcUnit.label.toLowerCase() => MBtcUnit
|
||||
case u if u == BtcUnit.code || u == BtcUnit.label.toLowerCase() => BtcUnit
|
||||
case u => throw new IllegalArgumentException(s"unhandled unit=$u")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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, _) =>
|
||||
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
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
-1
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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 grizzled.slf4j.Logging
|
||||
@ -23,24 +7,15 @@ import scala.util.{Failure, Success, Try}
|
||||
object DBCompatChecker extends Logging {
|
||||
|
||||
/**
|
||||
* Tests if the channels data in the DB are compatible with the current version of eclair; throws an exception if incompatible.
|
||||
* Tests if the DB files are compatible with the current version of eclair; throws an exception if incompatible.
|
||||
*
|
||||
* @param nodeParams
|
||||
*/
|
||||
def checkDBCompatibility(nodeParams: NodeParams): Unit =
|
||||
Try(nodeParams.db.channels.listLocalChannels()) match {
|
||||
Try(nodeParams.networkDb.listChannels() ++ nodeParams.networkDb.listNodes() ++ nodeParams.peersDb.listPeers() ++ nodeParams.channelsDb.listChannels()) match {
|
||||
case Success(_) => {}
|
||||
case Failure(_) => throw IncompatibleDBException
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the network database is readable.
|
||||
*
|
||||
* @param nodeParams
|
||||
*/
|
||||
def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit =
|
||||
Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes(), nodeParams.db.network.listChannelUpdates()) match {
|
||||
case Success(_) => {}
|
||||
case Failure(_) => throw IncompatibleNetworkDBException
|
||||
}
|
||||
}
|
||||
|
||||
case object IncompatibleDBException extends RuntimeException("DB files are not compatible with this version of eclair.")
|
||||
|
||||
@ -1,244 +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.UUID
|
||||
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.eclair.channel._
|
||||
import fr.acinq.eclair.db.{NetworkFee, IncomingPayment, 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.router.{ChannelDesc, RouteRequest, RouteResponse}
|
||||
import scodec.bits.ByteVector
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
|
||||
|
||||
case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress])
|
||||
|
||||
case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed])
|
||||
|
||||
trait Eclair {
|
||||
|
||||
def connect(uri: String)(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector])(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String]
|
||||
|
||||
def channelsInfo(toRemoteNode: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]]
|
||||
|
||||
def channelInfo(channelId: ByteVector32)(implicit timeout: Timeout): Future[RES_GETINFO]
|
||||
|
||||
def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]
|
||||
|
||||
def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String])(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: Option[Long] = None, maxAttempts: Option[Int] = 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 audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]
|
||||
|
||||
def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]]
|
||||
|
||||
def channelStats()(implicit timeout: Timeout): Future[Seq[Stats]]
|
||||
|
||||
def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]]
|
||||
|
||||
def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]]
|
||||
|
||||
def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]]
|
||||
|
||||
def allNodes()(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]]
|
||||
|
||||
def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]]
|
||||
|
||||
def allUpdates(nodeId: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]]
|
||||
|
||||
def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse]
|
||||
|
||||
}
|
||||
|
||||
class EclairImpl(appKit: Kit) extends Eclair {
|
||||
|
||||
implicit val ec = appKit.system.dispatcher
|
||||
|
||||
override def connect(uri: String)(implicit timeout: Timeout): Future[String] = {
|
||||
(appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]
|
||||
}
|
||||
|
||||
override def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: 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.map(MilliSatoshi).getOrElse(MilliSatoshi(0)),
|
||||
fundingTxFeeratePerKw_opt = fundingFeerateSatByte.map(feerateByte2Kw),
|
||||
channelFlags = flags.map(_.toByte),
|
||||
timeout_opt = Some(openTimeout))).mapTo[String]
|
||||
}
|
||||
|
||||
override def close(channelIdentifier: Either[ByteVector32, ShortChannelId], scriptPubKey: Option[ByteVector])(implicit timeout: Timeout): Future[String] = {
|
||||
sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_CLOSE(scriptPubKey)).mapTo[String]
|
||||
}
|
||||
|
||||
override def forceClose(channelIdentifier: Either[ByteVector32, ShortChannelId])(implicit timeout: Timeout): Future[String] = {
|
||||
sendToChannel(channelIdentifier.fold[String](_.toString(), _.toString()), CMD_FORCECLOSE).mapTo[String]
|
||||
}
|
||||
|
||||
override def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[String] = {
|
||||
sendToChannel(channelId, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)).mapTo[String]
|
||||
}
|
||||
|
||||
override def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {
|
||||
peers <- (appKit.switchboard ? 'peers).mapTo[Iterable[ActorRef]]
|
||||
peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
|
||||
} yield peerinfos
|
||||
|
||||
override def channelsInfo(toRemoteNode: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = toRemoteNode match {
|
||||
case Some(pk) => for {
|
||||
channelsId <- (appKit.register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys)
|
||||
channels <- Future.sequence(channelsId.map(channelId => sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
|
||||
} yield channels
|
||||
case None => for {
|
||||
channels_id <- (appKit.register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys)
|
||||
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toHex, CMD_GETINFO).mapTo[RES_GETINFO]))
|
||||
} yield channels
|
||||
}
|
||||
|
||||
override def channelInfo(channelId: ByteVector32)(implicit timeout: Timeout): Future[RES_GETINFO] = {
|
||||
sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO]
|
||||
}
|
||||
|
||||
override def allNodes()(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]]
|
||||
|
||||
override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = {
|
||||
(appKit.router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))
|
||||
}
|
||||
|
||||
override def allUpdates(nodeId: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]] = nodeId match {
|
||||
case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]]
|
||||
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: Option[Long], expire: Option[Long], fallbackAddress: Option[String])(implicit timeout: Timeout): Future[PaymentRequest] = {
|
||||
fallbackAddress.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.map(MilliSatoshi), expirySeconds_opt = expire, fallbackAddress = fallbackAddress)).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 send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None)(implicit timeout: Timeout): Future[UUID] = {
|
||||
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
|
||||
val sendPayment = minFinalCltvExpiry_opt match {
|
||||
case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts)
|
||||
case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts)
|
||||
}
|
||||
(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)
|
||||
}
|
||||
}
|
||||
|
||||
override def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] = Future {
|
||||
appKit.nodeParams.db.payments.getIncomingPayment(paymentHash)
|
||||
}
|
||||
|
||||
override def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse] = {
|
||||
val from = from_opt.getOrElse(0L)
|
||||
val to = to_opt.getOrElse(Long.MaxValue)
|
||||
|
||||
Future(AuditResponse(
|
||||
sent = appKit.nodeParams.db.audit.listSent(from, to),
|
||||
received = appKit.nodeParams.db.audit.listReceived(from, to),
|
||||
relayed = appKit.nodeParams.db.audit.listRelayed(from, to)
|
||||
))
|
||||
}
|
||||
|
||||
override def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]] = {
|
||||
val from = from_opt.getOrElse(0L)
|
||||
val to = to_opt.getOrElse(Long.MaxValue)
|
||||
|
||||
Future(appKit.nodeParams.db.audit.listNetworkFees(from, to))
|
||||
}
|
||||
|
||||
override def channelStats()(implicit timeout: Timeout): Future[Seq[Stats]] = Future(appKit.nodeParams.db.audit.stats)
|
||||
|
||||
override def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future {
|
||||
val from = from_opt.getOrElse(0L)
|
||||
val to = to_opt.getOrElse(Long.MaxValue)
|
||||
|
||||
appKit.nodeParams.db.payments.listPaymentRequests(from, to)
|
||||
}
|
||||
|
||||
override def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future {
|
||||
val from = from_opt.getOrElse(0L)
|
||||
val to = to_opt.getOrElse(Long.MaxValue)
|
||||
|
||||
appKit.nodeParams.db.payments.listPendingPaymentRequests(from, to)
|
||||
}
|
||||
|
||||
override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] = Future {
|
||||
appKit.nodeParams.db.payments.getPaymentRequest(paymentHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to a channel and expects a response
|
||||
*
|
||||
* @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded)
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
def sendToChannel(channelIdentifier: String, request: Any)(implicit timeout: Timeout): Future[Any] =
|
||||
for {
|
||||
fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request))
|
||||
.recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) }
|
||||
.recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) }
|
||||
res <- appKit.register ? fwdReq
|
||||
} yield res
|
||||
|
||||
override def getInfoResponse()(implicit timeout: Timeout): Future[GetInfoResponse] = Future.successful(
|
||||
GetInfoResponse(nodeId = appKit.nodeParams.nodeId,
|
||||
alias = appKit.nodeParams.alias,
|
||||
chainHash = appKit.nodeParams.chainHash,
|
||||
blockHeight = Globals.blockCount.intValue(),
|
||||
publicAddresses = appKit.nodeParams.publicAddresses)
|
||||
)
|
||||
|
||||
}
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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, FSM}
|
||||
|
||||
@ -1,55 +1,41 @@
|
||||
/*
|
||||
* 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.BitSet
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 13/02/2017.
|
||||
*/
|
||||
object Features {
|
||||
val OPTION_DATA_LOSS_PROTECT_MANDATORY = 0
|
||||
val OPTION_DATA_LOSS_PROTECT_OPTIONAL = 1
|
||||
|
||||
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
|
||||
//val INITIAL_ROUTING_SYNC_BIT_MANDATORY = 2
|
||||
val INITIAL_ROUTING_SYNC_BIT_MANDATORY = 2
|
||||
val INITIAL_ROUTING_SYNC_BIT_OPTIONAL = 3
|
||||
|
||||
val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6
|
||||
val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7
|
||||
|
||||
|
||||
def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit)
|
||||
|
||||
def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit)
|
||||
/**
|
||||
*
|
||||
* @param features feature bits
|
||||
* @return true if an initial dump of the routing table is requested
|
||||
*/
|
||||
def initialRoutingSync(features: BitSet): Boolean = features.get(INITIAL_ROUTING_SYNC_BIT_OPTIONAL)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param features feature bits
|
||||
* @return true if an initial dump of the routing table is requested
|
||||
*/
|
||||
def initialRoutingSync(features: BinaryData): Boolean = initialRoutingSync(BitSet.valueOf(features.reverse.toArray))
|
||||
|
||||
/**
|
||||
* Check that the features that we understand are correctly specified, and that there are no mandatory features that
|
||||
* we don't understand (even bits)
|
||||
*/
|
||||
def areSupported(bitset: BitSet): Boolean = {
|
||||
val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY)
|
||||
// for now there is no mandatory feature bit, so we don't support features with any even bit set
|
||||
for (i <- 0 until bitset.length() by 2) {
|
||||
if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false
|
||||
if (bitset.get(i)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -58,5 +44,5 @@ 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: BinaryData): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray))
|
||||
}
|
||||
|
||||
@ -1,24 +1,8 @@
|
||||
/*
|
||||
* 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.{AtomicLong, AtomicReference}
|
||||
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeratesPerKB, FeeratesPerKw}
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeratesPerByte, FeeratesPerKw}
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
@ -33,10 +17,10 @@ object Globals {
|
||||
val blockCount = new AtomicLong(0)
|
||||
|
||||
/**
|
||||
* This holds the current feerates, in satoshi-per-kilobytes.
|
||||
* This holds the current feerates, in satoshi-per-bytes.
|
||||
* The value is read by all actors, hence it needs to be thread-safe.
|
||||
*/
|
||||
val feeratesPerKB = new AtomicReference[FeeratesPerKB](null)
|
||||
val feeratesPerByte = new AtomicReference[FeeratesPerByte](null)
|
||||
|
||||
/**
|
||||
* This holds the current feerates, in satoshi-per-kw.
|
||||
|
||||
@ -1,44 +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.event.DiagnosticLoggingAdapter
|
||||
import akka.event.Logging.MDC
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
|
||||
object Logs {
|
||||
|
||||
def mdc(remoteNodeId_opt: Option[PublicKey] = None, channelId_opt: Option[ByteVector32] = None): MDC =
|
||||
Seq(
|
||||
remoteNodeId_opt.map(n => "nodeId" -> s" n:$n"), // nb: we preformat MDC values so that there is no white spaces in logs
|
||||
channelId_opt.map(c => "channelId" -> s" c:$c")
|
||||
).flatten.toMap
|
||||
|
||||
def withMdc(mdc: MDC)(f: => Any)(implicit log: DiagnosticLoggingAdapter) = {
|
||||
try {
|
||||
log.mdc(mdc)
|
||||
f
|
||||
} finally {
|
||||
log.clearMDC()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// we use a dedicated class so that the logging can be independently adjusted
|
||||
case class Diagnostics()
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.io.File
|
||||
@ -23,17 +7,12 @@ import java.sql.DriverManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet}
|
||||
import fr.acinq.eclair.NodeParams.WatcherType
|
||||
import fr.acinq.eclair.channel.Channel
|
||||
import fr.acinq.eclair.crypto.KeyManager
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.db.sqlite._
|
||||
import fr.acinq.eclair.router.RouterConf
|
||||
import fr.acinq.eclair.tor.Socks5ProxyParams
|
||||
import fr.acinq.eclair.wire.{Color, NodeAddress}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb, SqlitePreimagesDb}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
@ -41,46 +20,39 @@ import scala.concurrent.duration.FiniteDuration
|
||||
/**
|
||||
* Created by PM on 26/02/2017.
|
||||
*/
|
||||
case class NodeParams(keyManager: KeyManager,
|
||||
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
|
||||
privateKey: PrivateKey,
|
||||
alias: String,
|
||||
color: Color,
|
||||
publicAddresses: List[NodeAddress],
|
||||
globalFeatures: ByteVector,
|
||||
localFeatures: ByteVector,
|
||||
overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)],
|
||||
color: (Byte, Byte, Byte),
|
||||
publicAddresses: List[InetSocketAddress],
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData,
|
||||
dustLimitSatoshis: Long,
|
||||
maxHtlcValueInFlightMsat: UInt64,
|
||||
maxAcceptedHtlcs: Int,
|
||||
expiryDeltaBlocks: Int,
|
||||
htlcMinimumMsat: Int,
|
||||
toRemoteDelayBlocks: Int,
|
||||
maxToLocalDelayBlocks: Int,
|
||||
delayBlocks: Int,
|
||||
minDepthBlocks: Int,
|
||||
smartfeeNBlocks: Int,
|
||||
feeBaseMsat: Int,
|
||||
feeProportionalMillionth: Int,
|
||||
reserveToFundingRatio: Double,
|
||||
maxReserveToFundingRatio: Double,
|
||||
db: Databases,
|
||||
revocationTimeout: FiniteDuration,
|
||||
channelsDb: ChannelsDb,
|
||||
peersDb: PeersDb,
|
||||
networkDb: NetworkDb,
|
||||
preimagesDb: PreimagesDb,
|
||||
routerBroadcastInterval: FiniteDuration,
|
||||
routerValidateInterval: FiniteDuration,
|
||||
pingInterval: FiniteDuration,
|
||||
pingTimeout: FiniteDuration,
|
||||
pingDisconnect: Boolean,
|
||||
maxFeerateMismatch: Double,
|
||||
updateFeeMinDiffRatio: Double,
|
||||
autoReconnect: Boolean,
|
||||
chainHash: ByteVector32,
|
||||
chainHash: BinaryData,
|
||||
channelFlags: Byte,
|
||||
watcherType: WatcherType,
|
||||
paymentRequestExpiry: FiniteDuration,
|
||||
minFundingSatoshis: Long,
|
||||
routerConf: RouterConf,
|
||||
socksProxy_opt: Option[Socks5ProxyParams],
|
||||
maxPaymentAttempts: Int) {
|
||||
|
||||
val privateKey = keyManager.nodeKey.privateKey
|
||||
val nodeId = keyManager.nodeId
|
||||
}
|
||||
channelExcludeDuration: FiniteDuration,
|
||||
watcherType: WatcherType)
|
||||
|
||||
object NodeParams {
|
||||
|
||||
@ -88,6 +60,8 @@ object NodeParams {
|
||||
|
||||
object BITCOIND extends WatcherType
|
||||
|
||||
object BITCOINJ extends WatcherType
|
||||
|
||||
object ELECTRUM extends WatcherType
|
||||
|
||||
/**
|
||||
@ -103,128 +77,76 @@ object NodeParams {
|
||||
.withFallback(overrideDefaults)
|
||||
.withFallback(ConfigFactory.load()).getConfig("eclair")
|
||||
|
||||
def getSeed(datadir: File): ByteVector = {
|
||||
def makeNodeParams(datadir: File, config: Config): NodeParams = {
|
||||
|
||||
datadir.mkdirs()
|
||||
|
||||
val seedPath = new File(datadir, "seed.dat")
|
||||
seedPath.exists() match {
|
||||
case true => ByteVector(Files.readAllBytes(seedPath.toPath))
|
||||
val seed: BinaryData = seedPath.exists() match {
|
||||
case true => Files.readAllBytes(seedPath.toPath)
|
||||
case false =>
|
||||
datadir.mkdirs()
|
||||
val seed = randomKey.toBin
|
||||
Files.write(seedPath.toPath, seed.toArray)
|
||||
Files.write(seedPath.toPath, seed)
|
||||
seed
|
||||
}
|
||||
}
|
||||
|
||||
def makeChainHash(chain: String): ByteVector32 = {
|
||||
chain match {
|
||||
case "regtest" => Block.RegtestGenesisBlock.hash
|
||||
case "testnet" => Block.TestnetGenesisBlock.hash
|
||||
case "mainnet" => Block.LivenetGenesisBlock.hash
|
||||
case invalid => throw new RuntimeException(s"invalid chain '$invalid'")
|
||||
}
|
||||
}
|
||||
|
||||
def makeNodeParams(config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress], database: Databases): NodeParams = {
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
|
||||
|
||||
val chain = config.getString("chain")
|
||||
val chainHash = makeChainHash(chain)
|
||||
val chainHash = chain match {
|
||||
case "test" => Block.TestnetGenesisBlock.hash
|
||||
case "regtest" => Block.RegtestGenesisBlock.hash
|
||||
case _ => throw new RuntimeException("only regtest and testnet are supported for now")
|
||||
}
|
||||
|
||||
val color = ByteVector.fromValidHex(config.getString("node-color"))
|
||||
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(datadir, "eclair.sqlite")}")
|
||||
val channelsDb = new SqliteChannelsDb(sqlite)
|
||||
val peersDb = new SqlitePeersDb(sqlite)
|
||||
val networkDb = new SqliteNetworkDb(sqlite)
|
||||
val preimagesDb = new SqlitePreimagesDb(sqlite)
|
||||
|
||||
val color = BinaryData(config.getString("node-color"))
|
||||
require(color.size == 3, "color should be a 3-bytes hex buffer")
|
||||
|
||||
val watcherType = config.getString("watcher-type") match {
|
||||
case "bitcoinj" => BITCOINJ
|
||||
case "electrum" => ELECTRUM
|
||||
case _ => BITCOIND
|
||||
}
|
||||
|
||||
val dustLimitSatoshis = 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}")
|
||||
}
|
||||
|
||||
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")
|
||||
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 nodeAlias = config.getString("node-alias")
|
||||
require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)")
|
||||
|
||||
val overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)] = config.getConfigList("override-features").map { e =>
|
||||
val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
|
||||
val gf = ByteVector.fromValidHex(e.getString("global-features"))
|
||||
val lf = ByteVector.fromValidHex(e.getString("local-features"))
|
||||
(p -> (gf, lf))
|
||||
}.toMap
|
||||
|
||||
val socksProxy_opt = if (config.getBoolean("socks5.enabled")) {
|
||||
Some(Socks5ProxyParams(
|
||||
address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")),
|
||||
credentials_opt = None,
|
||||
randomizeCredentials = config.getBoolean("socks5.randomize-credentials"),
|
||||
useForIPv4 = config.getBoolean("socks5.use-for-ipv4"),
|
||||
useForIPv6 = config.getBoolean("socks5.use-for-ipv6"),
|
||||
useForTor = config.getBoolean("socks5.use-for-tor")
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
val addresses = config.getStringList("server.public-ips")
|
||||
.toList
|
||||
.map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt
|
||||
|
||||
NodeParams(
|
||||
keyManager = keyManager,
|
||||
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,
|
||||
extendedPrivateKey = extendedPrivateKey,
|
||||
privateKey = extendedPrivateKey.privateKey,
|
||||
alias = config.getString("node-alias").take(32),
|
||||
color = (color.data(0), color.data(1), color.data(2)),
|
||||
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
|
||||
globalFeatures = BinaryData(config.getString("global-features")),
|
||||
localFeatures = BinaryData(config.getString("local-features")),
|
||||
dustLimitSatoshis = config.getLong("dust-limit-satoshis"),
|
||||
maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")),
|
||||
maxAcceptedHtlcs = maxAcceptedHtlcs,
|
||||
maxAcceptedHtlcs = config.getInt("max-accepted-htlcs"),
|
||||
expiryDeltaBlocks = config.getInt("expiry-delta-blocks"),
|
||||
htlcMinimumMsat = config.getInt("htlc-minimum-msat"),
|
||||
toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"),
|
||||
maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"),
|
||||
delayBlocks = config.getInt("delay-blocks"),
|
||||
minDepthBlocks = config.getInt("mindepth-blocks"),
|
||||
smartfeeNBlocks = 3,
|
||||
feeBaseMsat = config.getInt("fee-base-msat"),
|
||||
feeProportionalMillionth = config.getInt("fee-proportional-millionths"),
|
||||
feeProportionalMillionth = config.getInt("fee-proportional-millionth"),
|
||||
reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"),
|
||||
maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"),
|
||||
db = database,
|
||||
revocationTimeout = FiniteDuration(config.getDuration("revocation-timeout").getSeconds, TimeUnit.SECONDS),
|
||||
channelsDb = channelsDb,
|
||||
peersDb = peersDb,
|
||||
networkDb = networkDb,
|
||||
preimagesDb = preimagesDb,
|
||||
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval").getSeconds, TimeUnit.SECONDS),
|
||||
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval").getSeconds, TimeUnit.SECONDS),
|
||||
pingInterval = FiniteDuration(config.getDuration("ping-interval").getSeconds, TimeUnit.SECONDS),
|
||||
pingTimeout = FiniteDuration(config.getDuration("ping-timeout").getSeconds, TimeUnit.SECONDS),
|
||||
pingDisconnect = config.getBoolean("ping-disconnect"),
|
||||
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),
|
||||
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"),
|
||||
autoReconnect = config.getBoolean("auto-reconnect"),
|
||||
chainHash = chainHash,
|
||||
channelFlags = config.getInt("channel-flags").toByte,
|
||||
watcherType = watcherType,
|
||||
paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS),
|
||||
minFundingSatoshis = config.getLong("min-funding-satoshis"),
|
||||
routerConf = RouterConf(
|
||||
channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
|
||||
routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS),
|
||||
randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"),
|
||||
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"),
|
||||
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"),
|
||||
searchRatioChannelAge = config.getDouble("router.path-finding.ratio-channel-age"),
|
||||
searchRatioChannelCapacity = config.getDouble("router.path-finding.ratio-channel-capacity")
|
||||
),
|
||||
socksProxy_opt = socksProxy_opt,
|
||||
maxPaymentAttempts = config.getInt("max-payment-attempts")
|
||||
)
|
||||
channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
|
||||
watcherType = watcherType)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,6 @@
|
||||
/*
|
||||
* 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.net.{InetAddress, InetSocketAddress, ServerSocket}
|
||||
import java.net.{InetAddress, ServerSocket}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
@ -28,12 +12,8 @@ object PortChecker {
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def checkAvailable(host: String, port: Int): Unit = checkAvailable(InetAddress.getByName(host), port)
|
||||
|
||||
def checkAvailable(socketAddress: InetSocketAddress): Unit = checkAvailable(socketAddress.getAddress, socketAddress.getPort)
|
||||
|
||||
def checkAvailable(address: InetAddress, port: Int): Unit = {
|
||||
Try(new ServerSocket(port, 50, address)) match {
|
||||
def checkAvailable(host: String, port: Int): Unit = {
|
||||
Try(new ServerSocket(port, 50, InetAddress.getByName(host))) match {
|
||||
case Success(socket) =>
|
||||
Try(socket.close())
|
||||
case Failure(_) =>
|
||||
@ -43,4 +23,4 @@ object PortChecker {
|
||||
|
||||
}
|
||||
|
||||
case class TCPBindException(port: Int) extends RuntimeException(s"could not bind to port $port")
|
||||
case class TCPBindException(port: Int) extends RuntimeException
|
||||
|
||||
@ -1,351 +1,200 @@
|
||||
/*
|
||||
* 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.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.sql.DriverManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import akka.Done
|
||||
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.pattern.after
|
||||
import akka.stream.{ActorMaterializer, BindFailedException}
|
||||
import akka.util.Timeout
|
||||
import com.softwaremill.sttp.okhttp.OkHttpFutureBackend
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
|
||||
import fr.acinq.eclair.api._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
|
||||
import fr.acinq.eclair.api.{GetInfoResponse, Service}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
|
||||
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress
|
||||
import fr.acinq.eclair.blockchain.electrum._
|
||||
import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.{BitcoinjKit, BitcoinjWallet, BitcoinjWatcher}
|
||||
import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet, ElectrumWallet, ElectrumWatcher}
|
||||
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, _}
|
||||
import fr.acinq.eclair.channel.Register
|
||||
import fr.acinq.eclair.crypto.LocalKeyManager
|
||||
import fr.acinq.eclair.db.Databases
|
||||
import fr.acinq.eclair.io.{Authenticator, Server, Switchboard}
|
||||
import fr.acinq.eclair.io.{Server, Switchboard}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion
|
||||
import fr.acinq.eclair.tor.{Controller, TorProtocolHandler}
|
||||
import fr.acinq.eclair.wire.NodeAddress
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.JArray
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent._
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
|
||||
|
||||
/**
|
||||
* Setup eclair from a data directory.
|
||||
*
|
||||
* Created by PM on 25/01/2016.
|
||||
*
|
||||
* @param datadir directory where eclair-core will write/read its data.
|
||||
* @param overrideDefaults use this parameter to programmatically override the node configuration .
|
||||
* @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file.
|
||||
*/
|
||||
class Setup(datadir: File,
|
||||
overrideDefaults: Config = ConfigFactory.empty(),
|
||||
seed_opt: Option[ByteVector] = None,
|
||||
db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging {
|
||||
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
implicit val ec = ExecutionContext.Implicits.global
|
||||
implicit val sttpBackend = OkHttpFutureBackend()
|
||||
class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), actorSystem: ActorSystem = ActorSystem()) extends Logging {
|
||||
|
||||
logger.info(s"hello!")
|
||||
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
|
||||
logger.info(s"datadir=${datadir.getCanonicalPath}")
|
||||
|
||||
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val nodeParams = NodeParams.makeNodeParams(datadir, config)
|
||||
val chain = config.getString("chain")
|
||||
|
||||
// early checks
|
||||
DBCompatChecker.checkDBCompatibility(nodeParams)
|
||||
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
|
||||
|
||||
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
|
||||
logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}")
|
||||
|
||||
logger.info(s"initializing secure random generator")
|
||||
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala)
|
||||
secureRandom.nextInt()
|
||||
|
||||
datadir.mkdirs()
|
||||
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir))
|
||||
val chain = config.getString("chain")
|
||||
val keyManager = new LocalKeyManager(seed, NodeParams.makeChainHash(chain))
|
||||
|
||||
val database = db match {
|
||||
case Some(d) => d
|
||||
case None => Databases.sqliteJDBC(new File(datadir, chain))
|
||||
}
|
||||
|
||||
val nodeParams = NodeParams.makeNodeParams(config, keyManager, initTor(), database)
|
||||
|
||||
val serverBindingAddress = new InetSocketAddress(
|
||||
config.getString("server.binding-ip"),
|
||||
config.getInt("server.port"))
|
||||
|
||||
// early checks
|
||||
DBCompatChecker.checkDBCompatibility(nodeParams)
|
||||
DBCompatChecker.checkNetworkDBCompatibility(nodeParams)
|
||||
PortChecker.checkAvailable(serverBindingAddress)
|
||||
|
||||
logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}")
|
||||
logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}")
|
||||
implicit val system = actorSystem
|
||||
implicit val materializer = ActorMaterializer()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
implicit val ec = ExecutionContext.Implicits.global
|
||||
|
||||
val bitcoin = nodeParams.watcherType match {
|
||||
case BITCOIND =>
|
||||
val bitcoinClient = new BasicBitcoinJsonRPCClient(
|
||||
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = config.getString("bitcoind.rpcuser"),
|
||||
password = config.getString("bitcoind.rpcpassword"),
|
||||
host = config.getString("bitcoind.host"),
|
||||
port = config.getInt("bitcoind.rpcport"))
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
port = config.getInt("bitcoind.rpcport")))
|
||||
val future = for {
|
||||
json <- bitcoinClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException }
|
||||
// Make sure wallet support is enabled in bitcoind.
|
||||
_ <- bitcoinClient.invoke("getbalance").recover { case _ => throw BitcoinWalletDisabledException }
|
||||
json <- bitcoinClient.rpcClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException }
|
||||
progress = (json \ "verificationprogress").extract[Double]
|
||||
blocks = (json \ "blocks").extract[Long]
|
||||
headers = (json \ "headers").extract[Long]
|
||||
chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse)
|
||||
bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[Int])
|
||||
unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) =>
|
||||
values
|
||||
.filter(value => (value \ "spendable").extract[Boolean])
|
||||
.map(value => (value \ "address").extract[String])
|
||||
}
|
||||
} yield (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers)
|
||||
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse))
|
||||
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
|
||||
} yield (progress, chainHash, bitcoinVersion)
|
||||
// blocking sanity checks
|
||||
val (progress, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds")
|
||||
assert(bitcoinVersion >= 170000, "Eclair requires Bitcoin Core 0.17.0 or higher")
|
||||
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
|
||||
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
|
||||
if (chainHash != Block.RegtestGenesisBlock.hash) {
|
||||
assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Make sure that all your UTXOS are segwit UTXOS and not p2pkh (check out our README for more details)")
|
||||
}
|
||||
assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress")
|
||||
assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks")
|
||||
assert(progress > 0.99, "bitcoind should be synchronized")
|
||||
// TODO: add a check on bitcoin version?
|
||||
Bitcoind(bitcoinClient)
|
||||
case BITCOINJ =>
|
||||
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
|
||||
val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList
|
||||
logger.info(s"using staticPeers=$staticPeers")
|
||||
val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers)
|
||||
bitcoinjKit.startAsync()
|
||||
Await.ready(bitcoinjKit.initialized, 10 seconds)
|
||||
Bitcoinj(bitcoinjKit)
|
||||
case ELECTRUM =>
|
||||
val addresses = config.hasPath("electrum") match {
|
||||
case true =>
|
||||
val host = config.getString("electrum.host")
|
||||
val port = config.getInt("electrum.port")
|
||||
val ssl = config.getString("electrum.ssl") match {
|
||||
case "off" => SSL.OFF
|
||||
case "loose" => SSL.LOOSE
|
||||
case _ => SSL.STRICT // strict mode is the default when we specify a custom electrum server, we don't want to be MITMed
|
||||
}
|
||||
val address = InetSocketAddress.createUnresolved(host, port)
|
||||
logger.info(s"override electrum default with server=$address ssl=$ssl")
|
||||
Set(ElectrumServerAddress(address, ssl))
|
||||
case false =>
|
||||
val (addressesFile, sslEnabled) = nodeParams.chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash => ("/electrum/servers_regtest.json", false) // in regtest we connect in plaintext
|
||||
case Block.TestnetGenesisBlock.hash => ("/electrum/servers_testnet.json", true)
|
||||
case Block.LivenetGenesisBlock.hash => ("/electrum/servers_mainnet.json", true)
|
||||
}
|
||||
val stream = classOf[Setup].getResourceAsStream(addressesFile)
|
||||
ElectrumClientPool.readServerAddresses(stream, sslEnabled)
|
||||
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
|
||||
val addressesFile = chain match {
|
||||
case "test" => "/electrum/servers_testnet.json"
|
||||
case "regtest" => "/electrum/servers_regtest.json"
|
||||
}
|
||||
val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(addresses)), "electrum-client", SupervisorStrategy.Resume))
|
||||
val stream = classOf[Setup].getResourceAsStream(addressesFile)
|
||||
val addresses = ElectrumClient.readServerAddresses(stream)
|
||||
val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClient(addresses)), "electrum-client", SupervisorStrategy.Resume))
|
||||
Electrum(electrumClient)
|
||||
}
|
||||
|
||||
def bootstrap: Future[Kit] = {
|
||||
val zmqConnected = Promise[Boolean]()
|
||||
val tcpBound = Promise[Unit]()
|
||||
|
||||
val defaultFeerates = FeeratesPerByte(block_1 = config.getLong("default-feerates.delay-blocks.1"), blocks_2 = config.getLong("default-feerates.delay-blocks.2"), blocks_6 = config.getLong("default-feerates.delay-blocks.6"), blocks_12 = config.getLong("default-feerates.delay-blocks.12"), blocks_36 = config.getLong("default-feerates.delay-blocks.36"), blocks_72 = config.getLong("default-feerates.delay-blocks.72"))
|
||||
Globals.feeratesPerByte.set(defaultFeerates)
|
||||
Globals.feeratesPerKw.set(FeeratesPerKw(defaultFeerates))
|
||||
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
|
||||
val feeProvider = (chain, bitcoin) match {
|
||||
case ("regtest", _) => new ConstantFeeProvider(defaultFeerates)
|
||||
case (_, Bitcoind(client)) => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
}
|
||||
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
|
||||
case feerates: FeeratesPerByte =>
|
||||
Globals.feeratesPerByte.set(feerates)
|
||||
Globals.feeratesPerKw.set(FeeratesPerKw(defaultFeerates))
|
||||
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
|
||||
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
|
||||
})
|
||||
|
||||
val watcher = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) =>
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
|
||||
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(bitcoinClient), "watcher", SupervisorStrategy.Resume))
|
||||
case Bitcoinj(bitcoinj) =>
|
||||
zmqConnected.success(true)
|
||||
system.actorOf(SimpleSupervisor.props(BitcoinjWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume))
|
||||
case Electrum(electrumClient) =>
|
||||
zmqConnected.success(true)
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
|
||||
}
|
||||
|
||||
val wallet = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
|
||||
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
|
||||
case Electrum(electrumClient) =>
|
||||
val electrumSeedPath = new File(datadir, "electrum_seed.dat")
|
||||
val electrumWallet = system.actorOf(ElectrumWallet.props(electrumSeedPath, electrumClient, ElectrumWallet.WalletParameters(Block.RegtestGenesisBlock.hash, allowSpendUnconfirmed = true)), "electrum-wallet")
|
||||
new ElectrumEclairWallet(electrumWallet)
|
||||
}
|
||||
wallet.getFinalAddress.map {
|
||||
case address => logger.info(s"initial wallet address=$address")
|
||||
}
|
||||
|
||||
val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
|
||||
case "local" => LocalPaymentHandler.props(nodeParams)
|
||||
case "noop" => Props[NoopPaymentHandler]
|
||||
}, "payment-handler", SupervisorStrategy.Resume))
|
||||
val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
|
||||
val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume))
|
||||
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
|
||||
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
|
||||
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart))
|
||||
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
|
||||
|
||||
val kit = Kit(
|
||||
nodeParams = nodeParams,
|
||||
system = system,
|
||||
watcher = watcher,
|
||||
paymentHandler = paymentHandler,
|
||||
register = register,
|
||||
relayer = relayer,
|
||||
router = router,
|
||||
switchboard = switchboard,
|
||||
paymentInitiator = paymentInitiator,
|
||||
server = server,
|
||||
wallet = wallet)
|
||||
|
||||
val api = new Service {
|
||||
|
||||
override def getInfoResponse: Future[GetInfoResponse] = Future.successful(GetInfoResponse(nodeId = nodeParams.privateKey.publicKey, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue()))
|
||||
|
||||
override def appKit = kit
|
||||
}
|
||||
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
|
||||
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
|
||||
}
|
||||
|
||||
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
|
||||
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
|
||||
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
|
||||
|
||||
for {
|
||||
_ <- Future.successful(true)
|
||||
feeratesRetrieved = Promise[Done]()
|
||||
zmqBlockConnected = Promise[Done]()
|
||||
zmqTxConnected = Promise[Done]()
|
||||
tcpBound = Promise[Done]()
|
||||
routerInitialized = Promise[Done]()
|
||||
|
||||
defaultFeerates = FeeratesPerKB(
|
||||
block_1 = config.getLong("default-feerates.delay-blocks.1"),
|
||||
blocks_2 = config.getLong("default-feerates.delay-blocks.2"),
|
||||
blocks_6 = config.getLong("default-feerates.delay-blocks.6"),
|
||||
blocks_12 = config.getLong("default-feerates.delay-blocks.12"),
|
||||
blocks_36 = config.getLong("default-feerates.delay-blocks.36"),
|
||||
blocks_72 = config.getLong("default-feerates.delay-blocks.72")
|
||||
)
|
||||
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 BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), 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()}")
|
||||
feeratesRetrieved.trySuccess(Done)
|
||||
})
|
||||
_ <- feeratesRetrieved.future
|
||||
|
||||
watcher = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) =>
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart))
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart))
|
||||
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoinClient))), "watcher", SupervisorStrategy.Resume))
|
||||
case Electrum(electrumClient) =>
|
||||
zmqBlockConnected.success(Done)
|
||||
zmqTxConnected.success(Done)
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
|
||||
}
|
||||
|
||||
router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher, Some(routerInitialized)), "router", SupervisorStrategy.Resume))
|
||||
routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out")))
|
||||
_ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil)
|
||||
|
||||
wallet = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient)
|
||||
case Electrum(electrumClient) =>
|
||||
// TODO: DRY
|
||||
val chaindir = new File(datadir, chain)
|
||||
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "wallet.sqlite")}")
|
||||
val walletDb = new SqliteWalletDb(sqlite)
|
||||
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet")
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
|
||||
}
|
||||
_ = wallet.getFinalAddress.map {
|
||||
case address => logger.info(s"initial wallet address=$address")
|
||||
}
|
||||
|
||||
audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume))
|
||||
paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
|
||||
case "local" => LocalPaymentHandler.props(nodeParams)
|
||||
case "noop" => Props[NoopPaymentHandler]
|
||||
}, "payment-handler", SupervisorStrategy.Resume))
|
||||
register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
|
||||
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume))
|
||||
authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume))
|
||||
switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
|
||||
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart))
|
||||
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, router, register), "payment-initiator", SupervisorStrategy.Restart))
|
||||
_ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart))
|
||||
|
||||
kit = Kit(
|
||||
nodeParams = nodeParams,
|
||||
system = system,
|
||||
watcher = watcher,
|
||||
paymentHandler = paymentHandler,
|
||||
register = register,
|
||||
relayer = relayer,
|
||||
router = router,
|
||||
switchboard = switchboard,
|
||||
paymentInitiator = paymentInitiator,
|
||||
server = server,
|
||||
wallet = wallet)
|
||||
|
||||
zmqBlockTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
|
||||
zmqTxTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
|
||||
tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("server.port"))))
|
||||
|
||||
_ <- Future.firstCompletedOf(zmqBlockConnected.future :: zmqBlockTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(zmqTxConnected.future :: zmqTxTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
|
||||
_ <- if (config.getBoolean("api.enabled")) {
|
||||
logger.info(s"json-rpc api enabled on port=${config.getInt("api.port")}")
|
||||
implicit val materializer = ActorMaterializer()
|
||||
val getInfo = GetInfoResponse(nodeId = nodeParams.nodeId,
|
||||
alias = nodeParams.alias,
|
||||
chainHash = nodeParams.chainHash,
|
||||
blockHeight = Globals.blockCount.intValue(),
|
||||
publicAddresses = nodeParams.publicAddresses)
|
||||
val apiPassword = config.getString("api.password") match {
|
||||
case "" => throw EmptyAPIPasswordException
|
||||
case valid => valid
|
||||
}
|
||||
val apiRoute = if (!config.getBoolean("api.use-old-api")) {
|
||||
new Service {
|
||||
override val actorSystem = kit.system
|
||||
override val mat = materializer
|
||||
override val password = apiPassword
|
||||
override val eclairApi: Eclair = new EclairImpl(kit)
|
||||
}.route
|
||||
} else {
|
||||
new OldService {
|
||||
override val scheduler = system.scheduler
|
||||
override val password = apiPassword
|
||||
override val getInfoResponse: Future[GetInfoResponse] = Future.successful(getInfo)
|
||||
override val appKit: Kit = kit
|
||||
override val socketHandler = makeSocketHandler(system)(materializer)
|
||||
}.route
|
||||
}
|
||||
val httpBound = Http().bindAndHandle(apiRoute, config.getString("api.binding-ip"), config.getInt("api.port")).recover {
|
||||
case _: BindFailedException => throw TCPBindException(config.getInt("api.port"))
|
||||
}
|
||||
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(TCPBindException(config.getInt("api.port"))))
|
||||
Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
|
||||
} else {
|
||||
Future.successful(logger.info("json-rpc api is disabled"))
|
||||
}
|
||||
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
|
||||
} yield kit
|
||||
|
||||
}
|
||||
|
||||
private def await[T](awaitable: Awaitable[T], atMost: Duration, messageOnTimeout: => String): T = try {
|
||||
Await.result(awaitable, atMost)
|
||||
} catch {
|
||||
case e: TimeoutException =>
|
||||
logger.error(messageOnTimeout)
|
||||
throw e
|
||||
}
|
||||
|
||||
private def initTor(): Option[NodeAddress] = {
|
||||
if (config.getBoolean("tor.enabled")) {
|
||||
val promiseTorAddress = Promise[NodeAddress]()
|
||||
val auth = config.getString("tor.auth") match {
|
||||
case "password" => TorProtocolHandler.Password(config.getString("tor.password"))
|
||||
case "safecookie" => TorProtocolHandler.SafeCookie()
|
||||
}
|
||||
val protocolHandlerProps = TorProtocolHandler.props(
|
||||
version = OnionServiceVersion(config.getString("tor.protocol")),
|
||||
authentication = auth,
|
||||
privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath,
|
||||
virtualPort = config.getInt("server.port"),
|
||||
onionAdded = Some(promiseTorAddress))
|
||||
|
||||
val controller = system.actorOf(SimpleSupervisor.props(Controller.props(
|
||||
address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")),
|
||||
protocolHandlerProps = protocolHandlerProps), "tor", SupervisorStrategy.Stop))
|
||||
|
||||
val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds")
|
||||
logger.info(s"Tor address $torAddress")
|
||||
Some(torAddress)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @formatter:off
|
||||
sealed trait Bitcoin
|
||||
case class Bitcoind(bitcoinClient: BasicBitcoinJsonRPCClient) extends Bitcoin
|
||||
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
|
||||
case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin
|
||||
case class Electrum(electrumClient: ActorRef) extends Bitcoin
|
||||
// @formatter:on
|
||||
|
||||
@ -364,11 +213,3 @@ case class Kit(nodeParams: NodeParams,
|
||||
case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")
|
||||
|
||||
case object BitcoinRPCConnectionException extends RuntimeException("could not connect to bitcoind using json-rpc")
|
||||
|
||||
case object BitcoinWalletDisabledException extends RuntimeException("bitcoind must have wallet support enabled")
|
||||
|
||||
case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api")
|
||||
|
||||
case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair")
|
||||
|
||||
case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair")
|
||||
|
||||
@ -1,52 +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
|
||||
|
||||
/**
|
||||
* A short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain.
|
||||
*
|
||||
* See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
|
||||
*
|
||||
*/
|
||||
case class ShortChannelId(private val id: Long) extends Ordered[ShortChannelId] {
|
||||
|
||||
def toLong: Long = id
|
||||
|
||||
override def toString: String = {
|
||||
val TxCoordinates(blockHeight, txIndex, outputIndex) = ShortChannelId.coordinates(this)
|
||||
s"${blockHeight}x${txIndex}x${outputIndex}"
|
||||
}
|
||||
|
||||
// we use an unsigned long comparison here
|
||||
override def compare(that: ShortChannelId): Int = (this.id + Long.MinValue).compareTo(that.id + Long.MinValue)
|
||||
}
|
||||
|
||||
object ShortChannelId {
|
||||
|
||||
def apply(s: String): ShortChannelId = s.split("x").toList match {
|
||||
case blockHeight :: txIndex :: outputIndex :: Nil => ShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt))
|
||||
case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s")
|
||||
}
|
||||
|
||||
def apply(blockHeight: Int, txIndex: Int, outputIndex: Int): ShortChannelId = ShortChannelId(toShortId(blockHeight, txIndex, outputIndex))
|
||||
|
||||
def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL)
|
||||
|
||||
def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(((shortChannelId.id >> 40) & 0xFFFFFF).toInt, ((shortChannelId.id >> 16) & 0xFFFFFF).toInt, (shortChannelId.id & 0xFFFF).toInt)
|
||||
}
|
||||
|
||||
case class TxCoordinates(blockHeight: Int, txIndex: Int, outputIndex: Int)
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy}
|
||||
@ -35,12 +19,7 @@ class SimpleSupervisor(childProps: Props, childName: String, strategy: Superviso
|
||||
}
|
||||
|
||||
// we allow at most <maxNrOfRetries> within <withinTimeRange>, otherwise the child actor is not restarted (this avoids restart loops)
|
||||
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = false, maxNrOfRetries = 100, withinTimeRange = 1 minute) {
|
||||
case t =>
|
||||
// log this as silent errors are dangerous
|
||||
log.error(t, s"supervisor caught error for child=$childName strategy=$strategy ")
|
||||
strategy
|
||||
}
|
||||
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true, maxNrOfRetries = 100, withinTimeRange = 1 minute) { case _ => strategy }
|
||||
}
|
||||
|
||||
object SimpleSupervisor {
|
||||
|
||||
@ -1,36 +1,16 @@
|
||||
/*
|
||||
* 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.math.BigInteger
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
case class UInt64(private val underlying: BigInt) extends Ordered[UInt64] {
|
||||
case class UInt64(underlying: BigInt) 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 = underlying.compare(o.underlying)
|
||||
|
||||
def toByteVector: ByteVector = ByteVector.view(underlying.toByteArray.takeRight(8))
|
||||
|
||||
def toBigInt: BigInt = underlying
|
||||
|
||||
override def toString: String = underlying.toString
|
||||
}
|
||||
|
||||
@ -41,7 +21,7 @@ object UInt64 {
|
||||
|
||||
val MaxValue = UInt64(MaxValueBigInt)
|
||||
|
||||
def apply(bin: ByteVector) = new UInt64(new BigInteger(1, bin.toArray))
|
||||
def apply(bin: BinaryData) = new UInt64(new BigInteger(1, bin))
|
||||
|
||||
def apply(value: Long) = new UInt64(BigInt(value))
|
||||
|
||||
|
||||
@ -1,37 +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.api
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.scaladsl.model.{ContentTypes, HttpResponse}
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
import akka.http.scaladsl.server.{Directives, Route}
|
||||
import fr.acinq.eclair.api.JsonSupport._
|
||||
import scala.concurrent.{Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
trait ExtraDirectives extends Directives {
|
||||
|
||||
// custom directive to fail with HTTP 404 (and JSON response) if the element was not found
|
||||
def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) {
|
||||
case Success(Some(t)) => complete(t)
|
||||
case Success(None) =>
|
||||
complete(HttpResponse(NotFound).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("Not found"))))
|
||||
case Failure(_) => reject
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,60 +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.api
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import scodec.bits.ByteVector
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object FormParamExtractors {
|
||||
|
||||
implicit val publicKeyUnmarshaller: Unmarshaller[String, PublicKey] = Unmarshaller.strict { rawPubKey =>
|
||||
PublicKey(ByteVector.fromValidHex(rawPubKey))
|
||||
}
|
||||
|
||||
implicit val binaryDataUnmarshaller: Unmarshaller[String, ByteVector] = Unmarshaller.strict { str =>
|
||||
ByteVector.fromValidHex(str)
|
||||
}
|
||||
|
||||
implicit val sha256HashUnmarshaller: Unmarshaller[String, ByteVector32] = Unmarshaller.strict { bin =>
|
||||
ByteVector32.fromValidHex(bin)
|
||||
}
|
||||
|
||||
implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest =>
|
||||
PaymentRequest.read(rawRequest)
|
||||
}
|
||||
|
||||
implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str =>
|
||||
ShortChannelId(str)
|
||||
}
|
||||
|
||||
implicit val javaUUIDUnmarshaller: Unmarshaller[String, UUID] = Unmarshaller.strict { str =>
|
||||
UUID.fromString(str)
|
||||
}
|
||||
|
||||
implicit val timeoutSecondsUnmarshaller: Unmarshaller[String, Timeout] = Unmarshaller.strict { str =>
|
||||
Timeout(str.toInt.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,219 +1,76 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.UUID
|
||||
import com.google.common.net.HostAndPort
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction}
|
||||
import fr.acinq.bitcoin.{BinaryData, Transaction}
|
||||
import fr.acinq.eclair.channel.State
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.db.OutgoingPaymentStatus
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
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.{ShortChannelId, UInt64}
|
||||
import org.json4s.JsonAST._
|
||||
import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
import org.json4s.CustomSerializer
|
||||
import org.json4s.JsonAST.{JNull, JString}
|
||||
|
||||
/**
|
||||
* JSON Serializers.
|
||||
* Note: in general, deserialization does not need to be implemented.
|
||||
* Created by PM on 28/01/2016.
|
||||
*/
|
||||
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 UInt64Serializer extends CustomSerializer[UInt64](format => ({ null }, {
|
||||
case x: UInt64 => JInt(x.toBigInt)
|
||||
}))
|
||||
|
||||
class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({ null }, {
|
||||
case x: MilliSatoshi => JInt(x.amount)
|
||||
}))
|
||||
|
||||
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 PointSerializer extends CustomSerializer[Point](format => ({ null }, {
|
||||
case x: Point => JString(x.toString())
|
||||
}))
|
||||
|
||||
class ScalarSerializer extends CustomSerializer[Scalar](format => ({ null }, {
|
||||
case x: Scalar => JString("XXX")
|
||||
}))
|
||||
|
||||
class TransactionSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ({ null }, {
|
||||
case x: Transaction => JString(x.toString())
|
||||
}))
|
||||
|
||||
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](ser = format => ({ null }, {
|
||||
case x: TransactionWithInputInfo => 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.amount)))
|
||||
}))
|
||||
|
||||
class ColorSerializer extends CustomSerializer[Color](format => ({ null }, {
|
||||
case c: Color => JString(c.toString)
|
||||
}))
|
||||
|
||||
class RouteResponseSerializer extends CustomSerializer[RouteResponse](format => ({ 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](format => ({ null }, {
|
||||
case t: Throwable if t.getMessage != null => JString(t.getMessage)
|
||||
case t: Throwable => JString(t.getClass.getSimpleName)
|
||||
}))
|
||||
|
||||
class FailureMessageSerializer extends CustomSerializer[FailureMessage](format => ({ null }, {
|
||||
case m: FailureMessage => JString(m.message)
|
||||
}))
|
||||
|
||||
class NodeAddressSerializer extends CustomSerializer[NodeAddress](format => ({ null},{
|
||||
case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString)
|
||||
}))
|
||||
|
||||
class DirectionSerializer extends CustomSerializer[Direction](format => ({ null },{
|
||||
case d: Direction => JString(d.toString)
|
||||
}))
|
||||
|
||||
class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ( {
|
||||
null
|
||||
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
|
||||
case JString(hex) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case p: PaymentRequest => {
|
||||
val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq
|
||||
val minFinalCltvExpiry = p.minFinalCltvExpiry.map(mfce => JField("minFinalCltvExpiry", JLong(mfce))).toSeq
|
||||
val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq
|
||||
case x: BinaryData => JString(x.toString())
|
||||
}
|
||||
))
|
||||
|
||||
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.toString()
|
||||
case Right(r) => r.toString()
|
||||
})),
|
||||
JField("paymentHash", JString(p.paymentHash.toString()))) ++
|
||||
expiry ++
|
||||
minFinalCltvExpiry ++
|
||||
amount
|
||||
class StateSerializer extends CustomSerializer[State](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: State => JString(x.toString())
|
||||
}
|
||||
))
|
||||
|
||||
JObject(fieldList)
|
||||
}
|
||||
}))
|
||||
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: ShaChain => JNull
|
||||
}
|
||||
))
|
||||
|
||||
class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, {
|
||||
case id: UUID => JString(id.toString)
|
||||
}))
|
||||
class PublicKeySerializer extends CustomSerializer[PublicKey](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: PublicKey => JString(x.toString())
|
||||
}
|
||||
))
|
||||
|
||||
class OutgoingPaymentStatusSerializer extends CustomSerializer[OutgoingPaymentStatus.Value](format => ({ null }, {
|
||||
case el: OutgoingPaymentStatus.Value => JString(el.toString)
|
||||
}))
|
||||
class PrivateKeySerializer extends CustomSerializer[PrivateKey](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: PrivateKey => JString("XXX")
|
||||
}
|
||||
))
|
||||
|
||||
object JsonSupport extends Json4sSupport {
|
||||
class PointSerializer extends CustomSerializer[Point](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: Point => JString(x.toString())
|
||||
}
|
||||
))
|
||||
|
||||
implicit val serialization = jackson.Serialization
|
||||
class ScalarSerializer extends CustomSerializer[Scalar](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: Scalar => JString("XXX")
|
||||
}
|
||||
))
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats +
|
||||
new ByteVectorSerializer +
|
||||
new ByteVector32Serializer +
|
||||
new UInt64Serializer +
|
||||
new MilliSatoshiSerializer +
|
||||
new ShortChannelIdSerializer +
|
||||
new StateSerializer +
|
||||
new ShaChainSerializer +
|
||||
new PublicKeySerializer +
|
||||
new PrivateKeySerializer +
|
||||
new ScalarSerializer +
|
||||
new PointSerializer +
|
||||
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 +
|
||||
new JavaUUIDSerializer +
|
||||
new OutgoingPaymentStatusSerializer
|
||||
|
||||
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
class TransactionWithInputInfoSerializer extends CustomSerializer[TransactionWithInputInfo](format => ( {
|
||||
case JString(x) if (false) => // NOT IMPLEMENTED
|
||||
???
|
||||
}, {
|
||||
case x: TransactionWithInputInfo => JString(Transaction.write(x.tx).toString())
|
||||
}
|
||||
))
|
||||
|
||||
@ -1,420 +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.api
|
||||
|
||||
import akka.NotUsed
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props, Scheduler}
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.model.ws.{Message, TextMessage}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.server.directives.Credentials
|
||||
import akka.http.scaladsl.server.directives.RouteDirectives.reject
|
||||
import akka.pattern.ask
|
||||
import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source}
|
||||
import akka.stream.{ActorMaterializer, OverflowStrategy}
|
||||
import akka.util.Timeout
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.channel._
|
||||
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._
|
||||
import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
|
||||
import fr.acinq.eclair.{AuditResponse, GetInfoResponse, Kit, ShortChannelId, feerateByte2Kw}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.{JBool, JInt, JString}
|
||||
import org.json4s.{JValue, jackson}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
// @formatter:off
|
||||
case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", method: String, params: Seq[JValue])
|
||||
case class Error(code: Int, message: String)
|
||||
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
|
||||
case class Status(node_id: String)
|
||||
trait RPCRejection extends Rejection {
|
||||
def requestId: String
|
||||
}
|
||||
final case class UnknownMethodRejection(requestId: String) extends RPCRejection
|
||||
final case class UnknownParamsRejection(requestId: String, message: String) extends RPCRejection
|
||||
final case class RpcValidationRejection(requestId: String, message: String) extends RPCRejection
|
||||
final case class ExceptionRejection(requestId: String, message: String) extends RPCRejection
|
||||
// @formatter:on
|
||||
|
||||
trait OldService extends Logging {
|
||||
|
||||
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
|
||||
|
||||
def scheduler: Scheduler
|
||||
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = org.json4s.DefaultFormats + new ByteVectorSerializer + new ByteVector32Serializer + new UInt64Serializer + new MilliSatoshiSerializer + new ShortChannelIdSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + 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
|
||||
implicit val timeout = Timeout(60 seconds)
|
||||
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
|
||||
|
||||
import Json4sSupport.{marshaller, unmarshaller}
|
||||
|
||||
def password: String
|
||||
|
||||
def appKit: Kit
|
||||
|
||||
val socketHandler: Flow[Message, TextMessage.Strict, NotUsed]
|
||||
|
||||
def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match {
|
||||
case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id))
|
||||
case _ => akka.pattern.after(1 second, using = scheduler)(Future.successful(None)) // force a 1 sec pause to deter brute force
|
||||
}
|
||||
|
||||
val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
|
||||
`Access-Control-Allow-Methods`(POST) ::
|
||||
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
|
||||
|
||||
val myExceptionHandler = ExceptionHandler {
|
||||
case t: Throwable =>
|
||||
extractRequest { _ =>
|
||||
logger.error(s"API call failed with cause=${t.getMessage}")
|
||||
complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(StatusCodes.InternalServerError.intValue, t.getMessage)), "-1"))
|
||||
}
|
||||
}
|
||||
|
||||
def completeRpcFuture(requestId: String, future: Future[AnyRef]): Route = onComplete(future) {
|
||||
case Success(s) => completeRpc(requestId, s)
|
||||
case Failure(t) => reject(ExceptionRejection(requestId, t.getLocalizedMessage))
|
||||
}
|
||||
|
||||
def completeRpc(requestId: String, result: AnyRef): Route = complete(JsonRPCRes(result, None, requestId))
|
||||
|
||||
val myRejectionHandler: RejectionHandler = RejectionHandler.newBuilder()
|
||||
.handleNotFound {
|
||||
complete(StatusCodes.NotFound, JsonRPCRes(null, Some(Error(StatusCodes.NotFound.intValue, "not found")), "-1"))
|
||||
}
|
||||
.handle {
|
||||
case _: AuthenticationFailedRejection ⇒ complete(StatusCodes.Unauthorized, JsonRPCRes(null, Some(Error(StatusCodes.Unauthorized.intValue, "Access restricted")), "-1"))
|
||||
case v: RpcValidationRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, v.message)), v.requestId))
|
||||
case ukm: UnknownMethodRejection ⇒ complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, "method not found")), ukm.requestId))
|
||||
case p: UnknownParamsRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"invalid parameters for this method, should be: ${p.message}")), p.requestId))
|
||||
case m: MalformedRequestContentRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"malformed parameters for this method: ${m.message}")), "-1"))
|
||||
case e: ExceptionRejection ⇒ complete(StatusCodes.BadRequest,
|
||||
JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, s"command failed: ${e.message}")), e.requestId))
|
||||
case r ⇒ logger.error(s"API call failed with cause=$r")
|
||||
complete(StatusCodes.BadRequest, JsonRPCRes(null, Some(Error(StatusCodes.BadRequest.intValue, r.toString)), "-1"))
|
||||
}
|
||||
.result()
|
||||
|
||||
val route: Route =
|
||||
respondWithDefaultHeaders(customHeaders) {
|
||||
withRequestTimeoutResponse(r => HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """)) {
|
||||
handleExceptions(myExceptionHandler) {
|
||||
handleRejections(myRejectionHandler) {
|
||||
authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ =>
|
||||
pathSingleSlash {
|
||||
post {
|
||||
entity(as[JsonRPCBody]) {
|
||||
req =>
|
||||
val kit = appKit
|
||||
import kit._
|
||||
|
||||
req.method match {
|
||||
// utility methods
|
||||
case "getinfo" => completeRpcFuture(req.id, getInfoResponse)
|
||||
case "help" => completeRpc(req.id, help)
|
||||
|
||||
// channel lifecycle methods
|
||||
case "connect" => req.params match {
|
||||
case JString(pubkey) :: JString(host) :: JInt(port) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(s"$pubkey@$host:$port"))).mapTo[String])
|
||||
case JString(uri) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[nodeId@host:port] or [nodeId, host, port]"))
|
||||
}
|
||||
case "open" => req.params match {
|
||||
case JString(nodeId) :: JInt(fundingSatoshis) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(0), fundingTxFeeratePerKw_opt = None, channelFlags = None, timeout_opt = None)).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), channelFlags = None, fundingTxFeeratePerKw_opt = None, timeout_opt = None)).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = None, timeout_opt = None)).mapTo[String])
|
||||
case JString(nodeId) :: JInt(fundingSatoshis) :: JInt(pushMsat) :: JInt(fundingFeerateSatPerByte) :: JInt(flags) :: Nil =>
|
||||
completeRpcFuture(req.id, (switchboard ? Peer.OpenChannel(PublicKey(ByteVector.fromValidHex(nodeId)), Satoshi(fundingSatoshis.toLong), MilliSatoshi(pushMsat.toLong), fundingTxFeeratePerKw_opt = Some(feerateByte2Kw(fundingFeerateSatPerByte.toLong)), channelFlags = Some(flags.toByte), timeout_opt = None)).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, s"[nodeId, fundingSatoshis], [nodeId, fundingSatoshis, pushMsat], [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte] or [nodeId, fundingSatoshis, pushMsat, feerateSatPerByte, flag]"))
|
||||
}
|
||||
case "close" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = None)).mapTo[String])
|
||||
case JString(identifier) :: JString(scriptPubKey) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_CLOSE(scriptPubKey = Some(ByteVector.fromValidHex(scriptPubKey)))).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId] or [channelId, scriptPubKey]"))
|
||||
}
|
||||
case "forceclose" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_FORCECLOSE).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
|
||||
}
|
||||
case "updaterelayfee" => req.params match {
|
||||
case JString(identifier) :: JInt(feeBaseMsat) :: JInt(feeProportionalMillionths) :: Nil =>
|
||||
completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String])
|
||||
case JString(identifier) :: JString(feeBaseMsat) :: JString(feeProportionalMillionths) :: Nil =>
|
||||
completeRpcFuture(req.id, sendToChannel(identifier, CMD_UPDATE_RELAY_FEE(feeBaseMsat.toLong, feeProportionalMillionths.toLong)).mapTo[String])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId] [feeBaseMsat] [feeProportionalMillionths]"))
|
||||
}
|
||||
// local network methods
|
||||
case "peers" => completeRpcFuture(req.id, for {
|
||||
peers <- (switchboard ? 'peers).mapTo[Iterable[ActorRef]]
|
||||
peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
|
||||
} yield peerinfos)
|
||||
case "channels" => req.params match {
|
||||
case Nil =>
|
||||
val f = for {
|
||||
channels_id <- (register ? 'channels).mapTo[Map[ByteVector32, ActorRef]].map(_.keys)
|
||||
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
|
||||
} yield channels
|
||||
completeRpcFuture(req.id, f)
|
||||
case JString(remoteNodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(remoteNodeId))) match {
|
||||
case Success(pk) =>
|
||||
val f = for {
|
||||
channels_id <- (register ? 'channelsTo).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys)
|
||||
channels <- Future.sequence(channels_id.map(channel_id => sendToChannel(channel_id.toString(), CMD_GETINFO).mapTo[RES_GETINFO]))
|
||||
} yield channels
|
||||
completeRpcFuture(req.id, f)
|
||||
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$remoteNodeId'"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "no arguments or [remoteNodeId]"))
|
||||
}
|
||||
case "channel" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, sendToChannel(identifier, CMD_GETINFO).mapTo[RES_GETINFO])
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[channelId]"))
|
||||
}
|
||||
|
||||
// global network methods
|
||||
case "allnodes" => completeRpcFuture(req.id, (router ? 'nodes).mapTo[Iterable[NodeAnnouncement]])
|
||||
case "allchannels" => completeRpcFuture(req.id, (router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))))
|
||||
case "allupdates" => req.params match {
|
||||
case JString(nodeId) :: Nil => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match {
|
||||
case Success(pk) => completeRpcFuture(req.id, (router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values))
|
||||
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid remote node id '$nodeId'"))
|
||||
}
|
||||
case _ => completeRpcFuture(req.id, (router ? 'updates).mapTo[Iterable[ChannelUpdate]])
|
||||
}
|
||||
|
||||
// payment methods
|
||||
case "receive" => req.params match {
|
||||
// only the payment description is given: user may want to generate a donation payment request
|
||||
case JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(None, description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
// the amount is now given with the description
|
||||
case JInt(amountMsat) :: JString(description) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description)).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
case JInt(amountMsat) :: JString(description) :: JInt(expirySeconds) :: Nil =>
|
||||
completeRpcFuture(req.id, (paymentHandler ? ReceivePayment(Some(MilliSatoshi(amountMsat.toLong)), description, Some(expirySeconds.toLong))).mapTo[PaymentRequest].map(PaymentRequest.write))
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[description] or [amount, description] or [amount, description, expiryDuration]"))
|
||||
}
|
||||
|
||||
// checkinvoice deprecated.
|
||||
case "parseinvoice" | "checkinvoice" => req.params match {
|
||||
case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(pr) => completeRpc(req.id,pr)
|
||||
case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getMessage}"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[payment_request]"))
|
||||
}
|
||||
|
||||
case "findroute" => req.params match {
|
||||
case JString(nodeId) :: JInt(amountMsat) :: Nil if nodeId.length() == 66 => Try(PublicKey(ByteVector.fromValidHex(nodeId))) match {
|
||||
case Success(pk) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, pk, amountMsat.toLong)).mapTo[RouteResponse])
|
||||
case Failure(_) => reject(RpcValidationRejection(req.id, s"invalid nodeId hash '$nodeId'"))
|
||||
}
|
||||
case JString(paymentRequest) :: Nil => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(PaymentRequest(_, Some(amountMsat), _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse])
|
||||
case Success(_) => reject(RpcValidationRejection(req.id, s"payment request is missing amount, please specify it"))
|
||||
case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}"))
|
||||
}
|
||||
case JString(paymentRequest) :: JInt(amountMsat) :: Nil => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(PaymentRequest(_, None, _, nodeId , _, _)) => completeRpcFuture(req.id, (router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat.toLong)).mapTo[RouteResponse])
|
||||
case Success(_) => reject(RpcValidationRejection(req.id, s"amount was specified both in payment request and api call"))
|
||||
case Failure(t) => reject(RpcValidationRejection(req.id, s"invalid payment request ${t.getLocalizedMessage}"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[payment_request] or [payment_request, amountMsat] or [nodeId, amountMsat]"))
|
||||
}
|
||||
|
||||
case "send" => req.params match {
|
||||
// user manually sets the payment information
|
||||
case JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil =>
|
||||
(Try(ByteVector32.fromValidHex(paymentHash)), Try(PublicKey(ByteVector.fromValidHex(nodeId)))) match {
|
||||
case (Success(ph), Success(pk)) => completeRpcFuture(req.id, (paymentInitiator ?
|
||||
SendPayment(amountMsat.toLong, ph, pk, maxAttempts = appKit.nodeParams.maxPaymentAttempts)).mapTo[PaymentResult].map {
|
||||
case s: PaymentSucceeded => s
|
||||
case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures))
|
||||
})
|
||||
case (Failure(_), _) => reject(RpcValidationRejection(req.id, s"invalid payment hash '$paymentHash'"))
|
||||
case _ => reject(RpcValidationRejection(req.id, s"invalid node id '$nodeId'"))
|
||||
}
|
||||
// user gives a Lightning payment request
|
||||
case JString(paymentRequest) :: rest => Try(PaymentRequest.read(paymentRequest)) match {
|
||||
case Success(pr) =>
|
||||
// setting the payment amount
|
||||
val amount_msat: Long = (pr.amount, rest) match {
|
||||
// optional amount always overrides the amount in the payment request
|
||||
case (_, JInt(amount_msat_override) :: Nil) => amount_msat_override.toLong
|
||||
case (Some(amount_msat_pr), _) => amount_msat_pr.amount
|
||||
case _ => throw new RuntimeException("you must manually specify an amount for this payment request")
|
||||
}
|
||||
logger.debug(s"api call for sending payment with amount_msat=$amount_msat")
|
||||
// optional cltv expiry
|
||||
val sendPayment = pr.minFinalCltvExpiry match {
|
||||
case None => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, maxAttempts = appKit.nodeParams.maxPaymentAttempts)
|
||||
case Some(minFinalCltvExpiry) => SendPayment(amount_msat, pr.paymentHash, pr.nodeId, assistedRoutes = Nil, minFinalCltvExpiry, maxAttempts = appKit.nodeParams.maxPaymentAttempts)
|
||||
}
|
||||
completeRpcFuture(req.id, (paymentInitiator ? sendPayment).mapTo[PaymentResult].map {
|
||||
case s: PaymentSucceeded => s
|
||||
case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures))
|
||||
})
|
||||
case _ => reject(RpcValidationRejection(req.id, s"payment request is not valid"))
|
||||
}
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[amountMsat, paymentHash, nodeId or [paymentRequest] or [paymentRequest, amountMsat]"))
|
||||
}
|
||||
|
||||
// check received payments
|
||||
case "checkpayment" => req.params match {
|
||||
case JString(identifier) :: Nil => completeRpcFuture(req.id, for {
|
||||
paymentHash <- Try(PaymentRequest.read(identifier)) match {
|
||||
case Success(pr) => Future.successful(pr.paymentHash)
|
||||
case _ => Try(ByteVector.fromValidHex(identifier)) match {
|
||||
case Success(s) => Future.successful(s)
|
||||
case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash"))
|
||||
}
|
||||
}
|
||||
found <- Future(appKit.nodeParams.db.payments.getIncomingPayment(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false)))
|
||||
} yield found)
|
||||
case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]"))
|
||||
}
|
||||
|
||||
// retrieve audit events
|
||||
case "audit" =>
|
||||
val (from, to) = req.params match {
|
||||
case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong)
|
||||
case _ => (0L, Long.MaxValue)
|
||||
}
|
||||
completeRpcFuture(req.id, Future(AuditResponse(
|
||||
sent = nodeParams.db.audit.listSent(from, to),
|
||||
received = nodeParams.db.audit.listReceived(from, to),
|
||||
relayed = nodeParams.db.audit.listRelayed(from, to))
|
||||
))
|
||||
|
||||
case "networkfees" =>
|
||||
val (from, to) = req.params match {
|
||||
case JInt(from) :: JInt(to) :: Nil => (from.toLong, to.toLong)
|
||||
case _ => (0L, Long.MaxValue)
|
||||
}
|
||||
completeRpcFuture(req.id, Future(nodeParams.db.audit.listNetworkFees(from, to)))
|
||||
|
||||
// retrieve fee stats
|
||||
case "channelstats" => completeRpcFuture(req.id, Future(nodeParams.db.audit.stats))
|
||||
|
||||
|
||||
// method name was not found
|
||||
case _ => reject(UnknownMethodRejection(req.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~ path("ws") {
|
||||
handleWebSocketMessages(socketHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getInfoResponse: Future[GetInfoResponse]
|
||||
|
||||
def makeSocketHandler(system: ActorSystem)(implicit materializer: ActorMaterializer): Flow[Message, TextMessage.Strict, NotUsed] = {
|
||||
|
||||
// create a flow transforming a queue of string -> string
|
||||
val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run()
|
||||
|
||||
// register an actor that feeds the queue when a payment is received
|
||||
system.actorOf(Props(new Actor {
|
||||
override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived])
|
||||
def receive: Receive = { case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) }
|
||||
}))
|
||||
|
||||
Flow[Message]
|
||||
.mapConcat(_ => Nil) // Ignore heartbeats and other data from the client
|
||||
.merge(flowOutput) // Stream the data we want to the client
|
||||
.map(TextMessage.apply)
|
||||
}
|
||||
|
||||
def help = List(
|
||||
"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")
|
||||
|
||||
/**
|
||||
* Sends a request to a channel and expects a response
|
||||
*
|
||||
* @param channelIdentifier can be a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded)
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
def sendToChannel(channelIdentifier: String, request: Any): Future[Any] =
|
||||
for {
|
||||
fwdReq <- Future(Register.ForwardShortId(ShortChannelId(channelIdentifier), request))
|
||||
.recoverWith { case _ => Future(Register.Forward(ByteVector32.fromValidHex(channelIdentifier), request)) }
|
||||
.recoverWith { case _ => Future.failed(new RuntimeException(s"invalid channel identifier '$channelIdentifier'")) }
|
||||
res <- appKit.register ? fwdReq
|
||||
} yield res
|
||||
}
|
||||
@ -1,295 +1,148 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import java.util.UUID
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.NotUsed
|
||||
import akka.actor.{Actor, ActorSystem, Props}
|
||||
import akka.http.scaladsl.model.HttpMethods.POST
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.actor.ActorRef
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
|
||||
import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`}
|
||||
import akka.http.scaladsl.model.ws.{Message, TextMessage}
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.server.directives.Credentials
|
||||
import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source}
|
||||
import akka.stream.{ActorMaterializer, OverflowStrategy}
|
||||
import akka.http.scaladsl.model.headers.HttpOriginRange.*
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.api.FormParamExtractors._
|
||||
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
|
||||
import fr.acinq.eclair.io.NodeURI
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _}
|
||||
import fr.acinq.eclair.{Eclair, ShortChannelId}
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.Kit
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import fr.acinq.eclair.payment.{PaymentRequest, PaymentResult, ReceivePayment, SendPayment}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.jackson.Serialization
|
||||
import scodec.bits.ByteVector
|
||||
import org.json4s.JsonAST.{JInt, JString}
|
||||
import org.json4s.{JValue, jackson}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
case class ErrorResponse(error: String)
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
|
||||
trait Service extends ExtraDirectives with Logging {
|
||||
// @formatter:off
|
||||
case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[JValue])
|
||||
case class Error(code: Int, message: String)
|
||||
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
|
||||
case class Status(node_id: String)
|
||||
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
|
||||
case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey , nodeId2: PublicKey)
|
||||
// @formatter:on
|
||||
|
||||
// important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541
|
||||
import JsonSupport.{formats, marshaller, serialization}
|
||||
trait Service extends Logging {
|
||||
|
||||
// used to send typed messages over the websocket
|
||||
val formatsWithTypeHint = formats.withTypeHintFieldName("type") +
|
||||
CustomTypeHints(Map(
|
||||
classOf[PaymentSent] -> "payment-sent",
|
||||
classOf[PaymentRelayed] -> "payment-relayed",
|
||||
classOf[PaymentReceived] -> "payment-received",
|
||||
classOf[PaymentSettlingOnChain] -> "payment-settling-onchain",
|
||||
classOf[PaymentFailed] -> "payment-failed"
|
||||
))
|
||||
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
|
||||
|
||||
def password: String
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = org.json4s.DefaultFormats + new BinaryDataSerializer + new StateSerializer + new ShaChainSerializer + new PublicKeySerializer + new PrivateKeySerializer + new ScalarSerializer + new PointSerializer + new TransactionWithInputInfoSerializer
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
|
||||
|
||||
val eclairApi: Eclair
|
||||
import Json4sSupport.{marshaller, unmarshaller}
|
||||
|
||||
implicit val actorSystem: ActorSystem
|
||||
implicit val mat: ActorMaterializer
|
||||
def appKit: Kit
|
||||
|
||||
// named and typed URL parameters used across several routes
|
||||
val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller)
|
||||
val nodeId = "nodeId".as[PublicKey]
|
||||
val shortChannelId = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller)
|
||||
val paymentHash = "paymentHash".as[ByteVector32](sha256HashUnmarshaller)
|
||||
val from = "from".as[Long]
|
||||
val to = "to".as[Long]
|
||||
val amountMsat = "amountMsat".as[Long]
|
||||
val invoice = "invoice".as[PaymentRequest]
|
||||
def getInfoResponse: Future[GetInfoResponse]
|
||||
|
||||
val apiExceptionHandler = ExceptionHandler {
|
||||
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-Origin`(*) ::
|
||||
`Access-Control-Allow-Headers`("Content-Type, Authorization") ::
|
||||
`Access-Control-Allow-Methods`(PUT, GET, POST, DELETE, OPTIONS) ::
|
||||
`Cache-Control`(public, `no-store`, `max-age`(0)) ::
|
||||
`Access-Control-Allow-Headers`("x-requested-with") :: Nil
|
||||
|
||||
// map all the rejections to a JSON error object ErrorResponse
|
||||
val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse {
|
||||
case res@HttpResponse(_, _, ent: HttpEntity.Strict, _) =>
|
||||
res.copy(entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String))))
|
||||
}
|
||||
def getChannel(channelId: String): Future[ActorRef] =
|
||||
for {
|
||||
channels <- (appKit.register ? 'channels).mapTo[Map[BinaryData, ActorRef]]
|
||||
} yield channels.get(BinaryData(channelId)).getOrElse(throw new RuntimeException("unknown channel"))
|
||||
|
||||
val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
|
||||
`Access-Control-Allow-Methods`(POST) ::
|
||||
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
|
||||
|
||||
lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = {
|
||||
|
||||
// create a flow transforming a queue of string -> string
|
||||
val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run()
|
||||
|
||||
// register an actor that feeds the queue on payment related events
|
||||
actorSystem.actorOf(Props(new Actor {
|
||||
|
||||
override def preStart: Unit = {
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentFailed])
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
|
||||
}
|
||||
|
||||
def receive: Receive = {
|
||||
case message: PaymentFailed => flowInput.offer(Serialization.write(message)(formatsWithTypeHint))
|
||||
case message: PaymentEvent => flowInput.offer(Serialization.write(message)(formatsWithTypeHint))
|
||||
}
|
||||
|
||||
}))
|
||||
|
||||
Flow[Message]
|
||||
.mapConcat(_ => Nil) // Ignore heartbeats and other data from the client
|
||||
.merge(flowOutput) // Stream the data we want to the client
|
||||
.map(TextMessage.apply)
|
||||
}
|
||||
|
||||
val timeoutResponse: HttpRequest => HttpResponse = { r =>
|
||||
HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out")))
|
||||
}
|
||||
|
||||
def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match {
|
||||
case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id))
|
||||
case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force
|
||||
}
|
||||
|
||||
val route: Route = {
|
||||
val route =
|
||||
respondWithDefaultHeaders(customHeaders) {
|
||||
handleExceptions(apiExceptionHandler) {
|
||||
handleRejections(apiRejectionHandler) {
|
||||
formFields("timeoutSeconds".as[Timeout].?) { tm_opt =>
|
||||
// this is the akka timeout
|
||||
implicit val timeout = tm_opt.getOrElse(Timeout(30 seconds))
|
||||
// we ensure that http timeout is greater than akka timeout
|
||||
withRequestTimeout(timeout.duration + 2.seconds) {
|
||||
withRequestTimeoutResponse(timeoutResponse) {
|
||||
authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ =>
|
||||
post {
|
||||
path("getinfo") {
|
||||
complete(eclairApi.getInfoResponse())
|
||||
} ~
|
||||
path("connect") {
|
||||
formFields("uri".as[String]) { uri =>
|
||||
complete(eclairApi.connect(uri))
|
||||
} ~ formFields(nodeId, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) =>
|
||||
complete(eclairApi.connect(s"$nodeId@$host:${port_opt.getOrElse(NodeURI.DEFAULT_PORT)}"))
|
||||
}
|
||||
} ~
|
||||
path("open") {
|
||||
formFields(nodeId, "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) {
|
||||
(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) =>
|
||||
complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt))
|
||||
}
|
||||
} ~
|
||||
path("updaterelayfee") {
|
||||
formFields(channelId, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) =>
|
||||
complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional))
|
||||
}
|
||||
} ~
|
||||
path("close") {
|
||||
formFields(channelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) =>
|
||||
complete(eclairApi.close(Left(channelId), scriptPubKey_opt))
|
||||
} ~ formFields(shortChannelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (shortChannelId, scriptPubKey_opt) =>
|
||||
complete(eclairApi.close(Right(shortChannelId), scriptPubKey_opt))
|
||||
}
|
||||
} ~
|
||||
path("forceclose") {
|
||||
formFields(channelId) { channelId =>
|
||||
complete(eclairApi.forceClose(Left(channelId)))
|
||||
} ~ formFields(shortChannelId) { shortChannelId =>
|
||||
complete(eclairApi.forceClose(Right(shortChannelId)))
|
||||
}
|
||||
} ~
|
||||
path("peers") {
|
||||
complete(eclairApi.peersInfo())
|
||||
} ~
|
||||
path("channels") {
|
||||
formFields(nodeId.?) { toRemoteNodeId_opt =>
|
||||
complete(eclairApi.channelsInfo(toRemoteNodeId_opt))
|
||||
}
|
||||
} ~
|
||||
path("channel") {
|
||||
formFields(channelId) { channelId =>
|
||||
complete(eclairApi.channelInfo(channelId))
|
||||
}
|
||||
} ~
|
||||
path("allnodes") {
|
||||
complete(eclairApi.allNodes())
|
||||
} ~
|
||||
path("allchannels") {
|
||||
complete(eclairApi.allChannels())
|
||||
} ~
|
||||
path("allupdates") {
|
||||
formFields(nodeId.?) { nodeId_opt =>
|
||||
complete(eclairApi.allUpdates(nodeId_opt))
|
||||
}
|
||||
} ~
|
||||
path("findroute") {
|
||||
formFields(invoice, amountMsat.?) {
|
||||
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo))
|
||||
case (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(nodeId, amountMsat) { (nodeId, amount) =>
|
||||
complete(eclairApi.findRoute(nodeId, amount))
|
||||
}
|
||||
} ~
|
||||
path("parseinvoice") {
|
||||
formFields(invoice) { invoice =>
|
||||
complete(invoice)
|
||||
}
|
||||
} ~
|
||||
path("payinvoice") {
|
||||
formFields(invoice, amountMsat.?, "maxAttempts".as[Int].?) {
|
||||
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts) =>
|
||||
complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts))
|
||||
case (invoice, Some(overrideAmount), maxAttempts) =>
|
||||
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts))
|
||||
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
|
||||
}
|
||||
} ~
|
||||
path("sendtonode") {
|
||||
formFields(amountMsat, paymentHash, nodeId, "maxAttempts".as[Int].?) { (amountMsat, paymentHash, nodeId, maxAttempts) =>
|
||||
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts = maxAttempts))
|
||||
}
|
||||
} ~
|
||||
path("getsentinfo") {
|
||||
formFields("id".as[UUID]) { id =>
|
||||
complete(eclairApi.sentInfo(Left(id)))
|
||||
} ~ formFields(paymentHash) { paymentHash =>
|
||||
complete(eclairApi.sentInfo(Right(paymentHash)))
|
||||
}
|
||||
} ~
|
||||
path("createinvoice") {
|
||||
formFields("description".as[String], amountMsat.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) =>
|
||||
complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress))
|
||||
}
|
||||
} ~
|
||||
path("getinvoice") {
|
||||
formFields(paymentHash) { paymentHash =>
|
||||
completeOrNotFound(eclairApi.getInvoice(paymentHash))
|
||||
}
|
||||
} ~
|
||||
path("listinvoices") {
|
||||
formFields(from.?, to.?) { (from_opt, to_opt) =>
|
||||
complete(eclairApi.allInvoices(from_opt, to_opt))
|
||||
}
|
||||
} ~
|
||||
path("listpendinginvoices") {
|
||||
formFields(from.?, to.?) { (from_opt, to_opt) =>
|
||||
complete(eclairApi.pendingInvoices(from_opt, to_opt))
|
||||
}
|
||||
} ~
|
||||
path("getreceivedinfo") {
|
||||
formFields(paymentHash) { paymentHash =>
|
||||
completeOrNotFound(eclairApi.receivedInfo(paymentHash))
|
||||
} ~ formFields(invoice) { invoice =>
|
||||
completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash))
|
||||
}
|
||||
} ~
|
||||
path("audit") {
|
||||
formFields(from.?, to.?) { (from_opt, to_opt) =>
|
||||
complete(eclairApi.audit(from_opt, to_opt))
|
||||
}
|
||||
} ~
|
||||
path("networkfees") {
|
||||
formFields(from.?, to.?) { (from_opt, to_opt) =>
|
||||
complete(eclairApi.networkFees(from_opt, to_opt))
|
||||
}
|
||||
} ~
|
||||
path("channelstats") {
|
||||
complete(eclairApi.channelStats())
|
||||
}
|
||||
} ~ get {
|
||||
path("ws") {
|
||||
handleWebSocketMessages(makeSocketHandler)
|
||||
}
|
||||
pathSingleSlash {
|
||||
post {
|
||||
entity(as[JsonRPCBody]) {
|
||||
req =>
|
||||
val kit = appKit
|
||||
import kit._
|
||||
val f_res: Future[AnyRef] = req match {
|
||||
case JsonRPCBody(_, _, "getinfo", _) => getInfoResponse
|
||||
case JsonRPCBody(_, _, "connect", JString(nodeId) :: JString(host) :: JInt(port) :: Nil) =>
|
||||
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), None)).mapTo[String]
|
||||
case JsonRPCBody(_, _, "open", JString(nodeId) :: JString(host) :: JInt(port) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: options) =>
|
||||
val channelFlags = options match {
|
||||
case JInt(value) :: Nil => Some(value.toByte)
|
||||
case _ => None // TODO: too lax?
|
||||
}
|
||||
}
|
||||
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), Some(NewChannel(Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags)))).mapTo[String]
|
||||
case JsonRPCBody(_, _, "peers", _) =>
|
||||
(switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]].map(_.map(_._1.toBin))
|
||||
case JsonRPCBody(_, _, "channels", _) =>
|
||||
(register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys)
|
||||
case JsonRPCBody(_, _, "channel", JString(channelId) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_GETINFO).mapTo[RES_GETINFO]
|
||||
case JsonRPCBody(_, _, "allnodes", _) =>
|
||||
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
|
||||
case JsonRPCBody(_, _, "allchannels", _) =>
|
||||
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
|
||||
case JsonRPCBody(_, _, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
|
||||
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
|
||||
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: rest) =>
|
||||
for {
|
||||
req <- Future(PaymentRequest.read(paymentRequest))
|
||||
amount = (req.amount, rest) match {
|
||||
case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided
|
||||
case (Some(amt), _) => amt.amount
|
||||
case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one
|
||||
case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request")
|
||||
}
|
||||
sendPayment = req.minFinalCltvExpiry match {
|
||||
case None => SendPayment(amount, req.paymentHash, req.nodeId)
|
||||
case Some(value) => SendPayment(amount, req.paymentHash, req.nodeId, value)
|
||||
}
|
||||
res <- (paymentInitiator ? sendPayment).mapTo[PaymentResult]
|
||||
} yield res
|
||||
case JsonRPCBody(_, _, "close", JString(channelId) :: JString(scriptPubKey) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String]
|
||||
case JsonRPCBody(_, _, "close", JString(channelId) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = None)).mapTo[String]
|
||||
case JsonRPCBody(_, _, "help", _) =>
|
||||
Future.successful(List(
|
||||
"connect (nodeId, host, port): connect to another lightning node through a secure connection",
|
||||
"open (nodeId, host, port, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
|
||||
"peers: list existing local peers",
|
||||
"channels: list existing local channels",
|
||||
"channel (channelId): retrieve detailed information about a given channel",
|
||||
"allnodes: list all known nodes",
|
||||
"allchannels: list all known channels",
|
||||
"receive (amountMsat, description): generate a payment request for a given amount",
|
||||
"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",
|
||||
"help: display this message"))
|
||||
case _ => Future.failed(new RuntimeException("method not found"))
|
||||
}
|
||||
|
||||
onComplete(f_res) {
|
||||
case Success(res) => complete(JsonRPCRes(res, None, req.id))
|
||||
case Failure(t) => complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(-1, t.getMessage)), req.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.blockchain
|
||||
|
||||
import fr.acinq.bitcoin.{Block, Transaction}
|
||||
|
||||
@ -1,23 +1,6 @@
|
||||
/*
|
||||
* 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.blockchain
|
||||
|
||||
import fr.acinq.bitcoin.{Satoshi, Transaction}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -30,7 +13,7 @@ trait EclairWallet {
|
||||
|
||||
def getFinalAddress: Future[String]
|
||||
|
||||
def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]
|
||||
def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]
|
||||
|
||||
/**
|
||||
* Committing *must* include publishing the transaction on the network.
|
||||
@ -53,17 +36,6 @@ trait EclairWallet {
|
||||
*/
|
||||
def rollback(tx: Transaction): Future[Boolean]
|
||||
|
||||
|
||||
/**
|
||||
* Tests whether the inputs of the provided transaction have been spent by another transaction.
|
||||
*
|
||||
* Implementations may always return false if they don't want to implement it
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
def doubleSpent(tx: Transaction): Future[Boolean]
|
||||
|
||||
}
|
||||
|
||||
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi)
|
||||
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)
|
||||
|
||||
@ -1,27 +1,10 @@
|
||||
/*
|
||||
* 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.blockchain
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, Script, ScriptWitness, Transaction}
|
||||
import fr.acinq.bitcoin.{BinaryData, Script, ScriptWitness, Transaction}
|
||||
import fr.acinq.eclair.channel.BitcoinEvent
|
||||
import fr.acinq.eclair.wire.ChannelAnnouncement
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
@ -35,35 +18,35 @@ sealed trait Watch {
|
||||
def channel: ActorRef
|
||||
def event: BitcoinEvent
|
||||
}
|
||||
// we need a public key script to use electrum apis
|
||||
final case class WatchConfirmed(channel: ActorRef, txId: ByteVector32, publicKeyScript: ByteVector, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
// we need a public key script to use bitcoinj or electrum apis
|
||||
final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeyScript: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
object WatchConfirmed {
|
||||
// if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript
|
||||
// we support both p2pkh and p2wpkh scripts
|
||||
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, tx.txOut.map(_.publicKeyScript).headOption.getOrElse(ByteVector.empty), minDepth, event)
|
||||
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
|
||||
|
||||
def extractPublicKeyScript(witness: ScriptWitness): ByteVector = Try(PublicKey(witness.stack.last)) match {
|
||||
def extractPublicKeyScript(witness: ScriptWitness): BinaryData = Try(PublicKey(witness.stack.last)) match {
|
||||
case Success(pubKey) =>
|
||||
// if last element of the witness is a public key, then this is a p2wpkh
|
||||
Script.write(Script.pay2wpkh(pubKey))
|
||||
case Failure(_) =>
|
||||
// otherwise this is a p2wsh
|
||||
Script.write(Script.pay2wsh(witness.stack.last))
|
||||
witness.stack.last
|
||||
}
|
||||
}
|
||||
|
||||
final case class WatchSpent(channel: ActorRef, txId: ByteVector32, outputIndex: Int, publicKeyScript: ByteVector, event: BitcoinEvent) extends Watch
|
||||
final case class WatchSpent(channel: ActorRef, txId: BinaryData, outputIndex: Int, publicKeyScript: BinaryData, event: BitcoinEvent) extends Watch
|
||||
object WatchSpent {
|
||||
// if we have the entire transaction, we can get the publicKeyScript from the relevant output
|
||||
def apply(channel: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent): WatchSpent = WatchSpent(channel, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event)
|
||||
}
|
||||
final case class WatchSpentBasic(channel: ActorRef, txId: ByteVector32, outputIndex: Int, publicKeyScript: ByteVector, event: BitcoinEvent) extends Watch // we use this when we don't care about the spending tx, and we also assume txid already exists
|
||||
final case class WatchSpentBasic(channel: ActorRef, txId: BinaryData, outputIndex: Int, publicKeyScript: BinaryData, event: BitcoinEvent) extends Watch // we use this when we don't care about the spending tx, and we also assume txid already exists
|
||||
object WatchSpentBasic {
|
||||
// if we have the entire transaction, we can get the publicKeyScript from the relevant output
|
||||
def apply(channel: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent): WatchSpentBasic = WatchSpentBasic(channel, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event)
|
||||
}
|
||||
// TODO: notify me if confirmation number gets below minDepth?
|
||||
final case class WatchLost(channel: ActorRef, txId: ByteVector32, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
final case class WatchLost(channel: ActorRef, txId: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
|
||||
trait WatchEvent {
|
||||
def event: BitcoinEvent
|
||||
@ -77,12 +60,8 @@ final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent
|
||||
* Publish the provided tx as soon as possible depending on locktime and csv
|
||||
*/
|
||||
final case class PublishAsap(tx: Transaction)
|
||||
final case class ValidateRequest(ann: ChannelAnnouncement)
|
||||
sealed trait UtxoStatus
|
||||
object UtxoStatus {
|
||||
case object Unspent extends UtxoStatus
|
||||
case class Spent(spendingTxConfirmed: Boolean) extends UtxoStatus
|
||||
}
|
||||
final case class ValidateResult(c: ChannelAnnouncement, fundingTx: Either[Throwable, (Transaction, UtxoStatus)])
|
||||
final case class ParallelGetRequest(ann: Seq[ChannelAnnouncement])
|
||||
final case class IndividualResult(c: ChannelAnnouncement, tx: Option[Transaction], unspent: Boolean)
|
||||
final case class ParallelGetResponse(r: Seq[IndividualResult])
|
||||
|
||||
// @formatter:on
|
||||
|
||||
@ -1,143 +1,214 @@
|
||||
/*
|
||||
* 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.blockchain.bitcoind
|
||||
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair._
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_OUTPUT_SPENT, BITCOIN_TX_CONFIRMED}
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST._
|
||||
import org.json4s.jackson.Serialization
|
||||
import scodec.bits.ByteVector
|
||||
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
|
||||
/**
|
||||
* Due to bitcoin-core wallet not fully supporting segwit txes yet, our current scheme is:
|
||||
* utxos <- parent-tx <- funding-tx
|
||||
*
|
||||
* With:
|
||||
* - utxos may be non-segwit
|
||||
* - parent-tx pays to a p2wpkh segwit output
|
||||
* - funding-tx is a segwit tx
|
||||
*
|
||||
* Created by PM on 06/07/2017.
|
||||
*/
|
||||
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
import BitcoinCoreWallet._
|
||||
override def getBalance: Future[Satoshi] = ???
|
||||
|
||||
def fundTransaction(hex: String, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = {
|
||||
val feeRatePerKB = BigDecimal(feerateKw2KB(feeRatePerKw))
|
||||
rpcClient.invoke("fundrawtransaction", hex, Options(lockUnspents, feeRatePerKB.bigDecimal.scaleByPowerOfTen(-8))).map(json => {
|
||||
override def getFinalAddress: Future[String] = rpcClient.invoke("getnewaddress").map(json => {
|
||||
val JString(address) = json
|
||||
address
|
||||
})
|
||||
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
|
||||
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
|
||||
case class MakeFundingTxResponseWithParent(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey)
|
||||
|
||||
def fundTransaction(hex: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
|
||||
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(lockUnspents)).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JInt(changepos) = json \ "changepos"
|
||||
val JDecimal(fee) = json \ "fee"
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), Satoshi(fee.bigDecimal.scaleByPowerOfTen(8).longValue()))
|
||||
val JDouble(fee) = json \ "fee"
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
|
||||
})
|
||||
}
|
||||
|
||||
def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw)
|
||||
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
|
||||
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
|
||||
|
||||
def signTransaction(hex: String): Future[SignTransactionResponse] =
|
||||
rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => {
|
||||
rpcClient.invoke("signrawtransaction", hex).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JBool(complete) = json \ "complete"
|
||||
if (!complete) {
|
||||
val message = (json \ "errors" \\ classOf[JString]).mkString(",")
|
||||
throw new JsonRPCError(Error(-1, message))
|
||||
}
|
||||
SignTransactionResponse(Transaction.read(hex), complete)
|
||||
})
|
||||
|
||||
def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toHex)
|
||||
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
|
||||
signTransaction(Transaction.write(tx).toString())
|
||||
|
||||
def getTransaction(txid: ByteVector32): Future[Transaction] = rpcClient.invoke("getrawtransaction", txid.toString()) collect { case JString(hex) => Transaction.read(hex) }
|
||||
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(Transaction.write(tx).toHex)
|
||||
|
||||
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid }
|
||||
|
||||
def unlockOutpoints(outPoints: Seq[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.toList.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result }
|
||||
|
||||
def isTransactionOutputSpendable(txId: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("gettxout", txId, outputIndex, true) collect { case j => j != JNull }
|
||||
|
||||
|
||||
override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDecimal(balance) => Satoshi(balance.bigDecimal.scaleByPowerOfTen(8).longValue()) }
|
||||
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
JString(address) <- rpcClient.invoke("getnewaddress")
|
||||
} yield address
|
||||
|
||||
private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = {
|
||||
val f = signTransaction(tx)
|
||||
// if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos
|
||||
f.recoverWith { case _ =>
|
||||
unlockOutpoints(tx.txIn.map(_.outPoint))
|
||||
.recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure
|
||||
.flatMap { case _ => f } // return signTransaction error
|
||||
.recoverWith { case _ => f } // return signTransaction error
|
||||
}
|
||||
def getTransaction(txid: BinaryData): Future[Transaction] = {
|
||||
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
|
||||
val JString(hex) = json
|
||||
Transaction.read(hex)
|
||||
})
|
||||
}
|
||||
|
||||
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
|
||||
// partial funding tx
|
||||
val partialFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
|
||||
publishTransaction(Transaction.write(tx).toString())
|
||||
|
||||
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
|
||||
rpcClient.invoke("sendrawtransaction", hex) collect {
|
||||
case JString(txid) => txid
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fundingTxResponse a funding tx response
|
||||
* @return an updated funding tx response that is properly sign
|
||||
*/
|
||||
def sign(fundingTxResponse: MakeFundingTxResponseWithParent): MakeFundingTxResponseWithParent = {
|
||||
// find the output that we are spending from
|
||||
val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
|
||||
|
||||
val pub = fundingTxResponse.priv.publicKey
|
||||
val pubKeyScript = Script.pay2pkh(pub)
|
||||
val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv)
|
||||
val witness = ScriptWitness(Seq(sig, pub.toBin))
|
||||
val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness)
|
||||
|
||||
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
fundingTxResponse.copy(fundingTx = fundingTx1)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key
|
||||
* that we need to re-sign the funding
|
||||
* @param newParentTx new parent tx
|
||||
* @return an updated funding transaction response where the funding tx now spends from newParentTx
|
||||
*/
|
||||
def replaceParent(fundingTxResponse: MakeFundingTxResponseWithParent, newParentTx: Transaction): MakeFundingTxResponseWithParent = {
|
||||
// find the output that we are spending from
|
||||
val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
|
||||
|
||||
// check that it matches what we expect, which is a P2WPKH output to our public key
|
||||
require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey))))
|
||||
|
||||
// update our tx input we the hash of the new parent
|
||||
val input = fundingTxResponse.fundingTx.txIn(0)
|
||||
val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash))
|
||||
val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1))
|
||||
|
||||
// and re-sign it
|
||||
sign(MakeFundingTxResponseWithParent(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv))
|
||||
}
|
||||
|
||||
def makeParentAndFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponseWithParent] =
|
||||
for {
|
||||
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
|
||||
FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw)
|
||||
// now let's sign the funding tx
|
||||
SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx)
|
||||
// there will probably be a change output, so we need to find which output is ours
|
||||
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None)
|
||||
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
|
||||
} yield MakeFundingTxResponse(fundingTx, outputIndex, fee)
|
||||
// ask for a new address and the corresponding private key
|
||||
JString(address) <- rpcClient.invoke("getnewaddress")
|
||||
JString(wif) <- rpcClient.invoke("dumpprivkey", address)
|
||||
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
|
||||
(prefix, raw) = Base58Check.decode(wif)
|
||||
priv = PrivateKey(raw, compressed = true)
|
||||
pub = priv.publicKey
|
||||
// create a tx that sends money to a P2SH(WPKH) output that matches our private key
|
||||
parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024)
|
||||
partialParentTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Nil,
|
||||
txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil,
|
||||
lockTime = 0L)
|
||||
FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx, lockUnspents = true)
|
||||
// this is the first tx that we will publish, a standard tx which send money to our p2wpkh address
|
||||
SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx)
|
||||
// now we create the funding tx
|
||||
partialFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
// and update it to spend from our segwit tx
|
||||
pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub)))
|
||||
unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil)
|
||||
} yield sign(MakeFundingTxResponseWithParent(parentTx, unsignedFundingTx, 0, priv))
|
||||
|
||||
/**
|
||||
* This is a workaround for malleability
|
||||
*
|
||||
* @param pubkeyScript
|
||||
* @param amount
|
||||
* @param feeRatePerKw
|
||||
* @return
|
||||
*/
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
|
||||
val promise = Promise[MakeFundingTxResponse]()
|
||||
(for {
|
||||
fundingTxResponse@MakeFundingTxResponseWithParent(parentTx, _, _, _) <- makeParentAndFundingTx(pubkeyScript, amount, feeRatePerKw)
|
||||
input0 = parentTx.txIn.head
|
||||
parentOfParentTx <- getTransaction(input0.outPoint.txid)
|
||||
_ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor")
|
||||
tempActor = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case WatchEventSpent(BITCOIN_OUTPUT_SPENT, spendingTx) =>
|
||||
if (parentTx.txid != spendingTx.txid) {
|
||||
// an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx)
|
||||
// set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx
|
||||
logger.warn(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}")
|
||||
}
|
||||
watcher ! WatchConfirmed(self, spendingTx.txid, spendingTx.txOut(0).publicKeyScript, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx))
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _) =>
|
||||
// a potential parent for our funding tx has been confirmed, let's update our funding tx
|
||||
val finalFundingTx = replaceParent(fundingTxResponse, tx)
|
||||
promise.success(MakeFundingTxResponse(finalFundingTx.fundingTx, finalFundingTx.fundingTxOutputIndex))
|
||||
}
|
||||
}))
|
||||
// we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar
|
||||
_ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, parentOfParentTx.txOut(input0.outPoint.index.toInt).publicKeyScript, BITCOIN_OUTPUT_SPENT)
|
||||
// and we publish the parent tx
|
||||
_ = logger.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}")
|
||||
// we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings)
|
||||
_ = system.scheduler.scheduleOnce(100 milliseconds, watcher, PublishAsap(parentTx))
|
||||
} yield {}) onFailure {
|
||||
case t: Throwable => promise.failure(t)
|
||||
}
|
||||
promise.future
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
|
||||
.map(_ => true) // if bitcoind says OK, then we consider the tx successfully published
|
||||
.recoverWith { case JsonRPCError(e) =>
|
||||
logger.warn(s"txid=${tx.txid} error=$e")
|
||||
getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
}
|
||||
.map(_ => true) // if bitcoind says OK, then we consider the tx succesfully published
|
||||
.recoverWith { case JsonRPCError(_) => getTransaction(tx.txid).map(_ => true).recover { case _ => false } } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
.recover { case _ => true } // in all other cases we consider that the tx has been published
|
||||
|
||||
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoints(tx.txIn.map(_.outPoint)) // we unlock all utxos used by the tx
|
||||
|
||||
override def doubleSpent(tx: Transaction): Future[Boolean] =
|
||||
for {
|
||||
exists <- getTransaction(tx.txid).map(_ => true).recover { case _ => false }
|
||||
doublespent <- if (exists) {
|
||||
// if the tx is in the blockchain, it can't have been doublespent
|
||||
Future.successful(false)
|
||||
} else {
|
||||
// if the tx wasn't in the blockchain and one of it's input has been spent, it is doublespent
|
||||
Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpendable(txIn.outPoint.txid.toHex, txIn.outPoint.index.toInt))).map(_.exists(_ == false))
|
||||
}
|
||||
} yield doublespent // TODO: should we check confirmations of the overriding tx?
|
||||
|
||||
/**
|
||||
* We currently only put a lock on the parent tx inputs, and we publish the parent tx immediately so there is nothing
|
||||
* to do here.
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true)
|
||||
}
|
||||
|
||||
object BitcoinCoreWallet {
|
||||
|
||||
// @formatter:off
|
||||
case class Options(lockUnspents: Boolean, feeRate: BigDecimal)
|
||||
case class Utxo(txid: String, vout: Long)
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Satoshi)
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
// @formatter:on
|
||||
case class Options(lockUnspents: Boolean)
|
||||
|
||||
}
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.blockchain.bitcoind
|
||||
|
||||
import java.util.concurrent.Executors
|
||||
@ -26,9 +10,8 @@ import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.collection.{Set, SortedMap}
|
||||
import scala.collection.SortedMap
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.Try
|
||||
@ -41,7 +24,7 @@ import scala.util.Try
|
||||
*/
|
||||
class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
|
||||
|
||||
import ZmqWatcher._
|
||||
import ZmqWatcher.TickNewBlock
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
|
||||
|
||||
@ -50,31 +33,27 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext =
|
||||
|
||||
case class TriggerEvent(w: Watch, e: WatchEvent)
|
||||
|
||||
def receive: Receive = watching(Set(), Map(), SortedMap(), None)
|
||||
def receive: Receive = watching(Set(), SortedMap(), None)
|
||||
|
||||
def watching(watches: Set[Watch], watchedUtxos: Map[OutPoint, Set[Watch]], block2tx: SortedMap[Long, Seq[Transaction]], nextTick: Option[Cancellable]): Receive = {
|
||||
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], nextTick: Option[Cancellable]): Receive = {
|
||||
|
||||
case NewTransaction(tx) =>
|
||||
log.debug(s"analyzing txid={} tx={}", tx.txid, tx)
|
||||
tx.txIn
|
||||
.map(_.outPoint)
|
||||
.flatMap(watchedUtxos.get)
|
||||
.flatten // List[Watch] -> Watch
|
||||
.collect {
|
||||
case w: WatchSpentBasic =>
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(w.event))
|
||||
case w: WatchSpent =>
|
||||
self ! TriggerEvent(w, WatchEventSpent(w.event, tx))
|
||||
//log.debug(s"analyzing txid=${tx.txid} tx=${Transaction.write(tx)}")
|
||||
watches.collect {
|
||||
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(event))
|
||||
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpent(event, tx))
|
||||
}
|
||||
|
||||
case NewBlock(block) =>
|
||||
// using a Try because in tests we generate fake blocks
|
||||
log.debug(s"received blockid=${Try(block.blockId).getOrElse(ByteVector32(ByteVector.empty))}")
|
||||
log.debug(s"received blockid=${Try(block.blockId).getOrElse(BinaryData(""))}")
|
||||
nextTick.map(_.cancel()) // this may fail or succeed, worse case scenario we will have two ticks in a row (no big deal)
|
||||
log.debug(s"scheduling a new task to check on tx confirmations")
|
||||
// we do this to avoid herd effects in testing when generating a lots of blocks in a row
|
||||
val task = context.system.scheduler.scheduleOnce(2 seconds, self, TickNewBlock)
|
||||
context become watching(watches, watchedUtxos, block2tx, Some(task))
|
||||
context become watching(watches, block2tx, Some(task))
|
||||
|
||||
case TickNewBlock =>
|
||||
client.getBlockCount.map {
|
||||
@ -83,72 +62,41 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext =
|
||||
Globals.blockCount.set(count)
|
||||
context.system.eventStream.publish(CurrentBlockCount(count))
|
||||
}
|
||||
/*client.estimateSmartFee(nodeParams.smartfeeNBlocks).map {
|
||||
case feeratePerKB if feeratePerKB > 0 =>
|
||||
val feeratePerKw = feerateKB2Kw(feeratePerKB)
|
||||
log.debug(s"setting feeratePerKB=$feeratePerKB -> feeratePerKw=$feeratePerKw")
|
||||
Globals.feeratePerKw.set(feeratePerKw)
|
||||
context.system.eventStream.publish(CurrentFeerate(feeratePerKw))
|
||||
case _ => () // bitcoind cannot estimate feerate
|
||||
}*/
|
||||
// TODO: beware of the herd effect
|
||||
watches.collect { case w: WatchConfirmed => checkConfirmed(w) }
|
||||
context become watching(watches, watchedUtxos, block2tx, None)
|
||||
watches.collect {
|
||||
case w@WatchConfirmed(_, txId, _, minDepth, event) =>
|
||||
log.debug(s"checking confirmations of txid=$txId")
|
||||
client.getTxConfirmations(txId.toString).map {
|
||||
case Some(confirmations) if confirmations >= minDepth =>
|
||||
client.getTransactionShortId(txId.toString).map {
|
||||
case (height, index) => self ! TriggerEvent(w, WatchEventConfirmed(event, height, index))
|
||||
}
|
||||
}
|
||||
}
|
||||
context become (watching(watches, block2tx, None))
|
||||
|
||||
case TriggerEvent(w, e) if watches.contains(w) =>
|
||||
log.info(s"triggering $w")
|
||||
w.channel ! e
|
||||
w match {
|
||||
case _: WatchSpent =>
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
()
|
||||
case _ =>
|
||||
context become watching(watches - w, removeWatchedUtxos(watchedUtxos, w), block2tx, None)
|
||||
}
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
if (!w.isInstanceOf[WatchSpent]) context.become(watching(watches - w, block2tx, None))
|
||||
|
||||
case CurrentBlockCount(count) => {
|
||||
val toPublish = block2tx.filterKeys(_ <= count)
|
||||
toPublish.values.flatten.map(tx => publish(tx))
|
||||
context become watching(watches, watchedUtxos, block2tx -- toPublish.keys, None)
|
||||
context.become(watching(watches, block2tx -- toPublish.keys, None))
|
||||
}
|
||||
|
||||
case w: Watch if !watches.contains(w) =>
|
||||
w match {
|
||||
case WatchSpentBasic(_, txid, outputIndex, _, _) =>
|
||||
// not: we assume parent tx was published, we just need to make sure this particular output has not been spent
|
||||
client.isTransactionOutputSpendable(txid.toString(), outputIndex, true).collect {
|
||||
case false =>
|
||||
log.info(s"output=$outputIndex of txid=$txid has already been spent")
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(w.event))
|
||||
}
|
||||
|
||||
case WatchSpent(_, txid, outputIndex, _, _) =>
|
||||
// first let's see if the parent tx was published or not
|
||||
client.getTxConfirmations(txid.toString()).collect {
|
||||
case Some(_) =>
|
||||
// parent tx was published, we need to make sure this particular output has not been spent
|
||||
client.isTransactionOutputSpendable(txid.toString(), outputIndex, true).collect {
|
||||
case false =>
|
||||
log.info(s"$txid:$outputIndex has already been spent, looking for the spending tx in the mempool")
|
||||
client.getMempool().map { mempoolTxs =>
|
||||
mempoolTxs.filter(tx => tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match {
|
||||
case Nil =>
|
||||
log.warning(s"$txid:$outputIndex has already been spent, spending tx not in the mempool, looking in the blockchain...")
|
||||
client.lookForSpendingTx(None, txid.toString(), outputIndex).map { tx =>
|
||||
log.warning(s"found the spending tx of $txid:$outputIndex in the blockchain: txid=${tx.txid}")
|
||||
self ! NewTransaction(tx)
|
||||
}
|
||||
case txs =>
|
||||
log.info(s"found ${txs.size} txs spending $txid:$outputIndex in the mempool: txids=${txs.map(_.txid).mkString(",")}")
|
||||
txs.foreach(tx => self ! NewTransaction(tx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case w: WatchConfirmed => checkConfirmed(w) // maybe the tx is already tx, in that case the watch will be triggered and removed immediately
|
||||
|
||||
case _: WatchLost => () // TODO: not implemented
|
||||
|
||||
case w => log.warning(s"ignoring $w")
|
||||
}
|
||||
|
||||
log.debug(s"adding watch $w for $sender")
|
||||
context.watch(w.channel)
|
||||
context become watching(watches + w, addWatchedUtxos(watchedUtxos, w), block2tx, nextTick)
|
||||
case w: Watch if !watches.contains(w) => addWatch(w, watches, block2tx)
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
@ -157,13 +105,13 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext =
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=$tx")
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
|
||||
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
|
||||
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
|
||||
} else if (cltvTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context become watching(watches, watchedUtxos, block2tx1, None)
|
||||
context.become(watching(watches, block2tx1, None))
|
||||
} else publish(tx)
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
|
||||
@ -174,44 +122,79 @@ class ZmqWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext =
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context become watching(watches, watchedUtxos, block2tx1, None)
|
||||
context.become(watching(watches, block2tx1, None))
|
||||
} else publish(tx)
|
||||
|
||||
case ValidateRequest(ann) => client.validate(ann).pipeTo(sender)
|
||||
case ParallelGetRequest(ann) => client.getParallel(ann).pipeTo(sender)
|
||||
|
||||
case Terminated(channel) =>
|
||||
// we remove watches associated to dead actor
|
||||
val deprecatedWatches = watches.filter(_.channel == channel)
|
||||
val watchedUtxos1 = deprecatedWatches.foldLeft(watchedUtxos) { case (m, w) => removeWatchedUtxos(m, w) }
|
||||
context.become(watching(watches -- deprecatedWatches, watchedUtxos1, block2tx, None))
|
||||
context.become(watching(watches -- deprecatedWatches, block2tx, None))
|
||||
|
||||
case 'watches => sender ! watches
|
||||
|
||||
}
|
||||
|
||||
def addWatch(w: Watch, watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]]) = {
|
||||
w match {
|
||||
case WatchSpentBasic(_, txid, outputIndex, _, _) =>
|
||||
// not: we assume parent tx was published, we just need to make sure this particular output has not been spent
|
||||
client.isTransactionOuputSpendable(txid.toString(), outputIndex, true).collect {
|
||||
case false =>
|
||||
log.warning(s"output=$outputIndex of txid=$txid has already been spent")
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(w.event))
|
||||
}
|
||||
|
||||
case w@WatchSpent(_, txid, outputIndex, _, _) =>
|
||||
// first let's see if the parent tx was published or not
|
||||
client.getTxConfirmations(txid.toString()).collect {
|
||||
case Some(_) =>
|
||||
// parent tx was published, we need to make sure this particular output has not been spent
|
||||
client.isTransactionOuputSpendable(txid.toString(), outputIndex, true).collect {
|
||||
case false =>
|
||||
log.warning(s"output=$outputIndex of txid=$txid has already been spent")
|
||||
client.getTxBlockHash(txid.toString()).collect {
|
||||
case Some(blockhash) =>
|
||||
log.warning(s"getting all transactions since blockhash=$blockhash")
|
||||
client.getTxsSinceBlockHash(blockhash).map {
|
||||
case txs =>
|
||||
log.warning(s"found ${txs.size} txs since blockhash=$blockhash")
|
||||
txs.foreach(tx => self ! NewTransaction(tx))
|
||||
} onFailure {
|
||||
case t: Throwable => log.error(t, "")
|
||||
}
|
||||
}
|
||||
client.getMempool().map {
|
||||
case txs =>
|
||||
log.warning(s"found ${txs.size} txs in the mempool")
|
||||
txs.foreach(tx => self ! NewTransaction(tx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case w: WatchConfirmed => self ! TickNewBlock
|
||||
|
||||
case w => log.warning(s"ignoring $w (not implemented)")
|
||||
}
|
||||
log.debug(s"adding watch $w for $sender")
|
||||
context.watch(w.channel)
|
||||
context.become(watching(watches + w, block2tx, None))
|
||||
}
|
||||
|
||||
// NOTE: we use a single thread to publish transactions so that it preserves order.
|
||||
// CHANGING THIS WILL RESULT IN CONCURRENCY ISSUES WHILE PUBLISHING PARENT AND CHILD TXS
|
||||
val singleThreadExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())
|
||||
|
||||
def publish(tx: Transaction, isRetry: Boolean = false): Unit = {
|
||||
log.info(s"publishing tx (isRetry=$isRetry): txid=${tx.txid} tx=$tx")
|
||||
log.info(s"publishing tx (isRetry=$isRetry): txid=${tx.txid} tx=${Transaction.write(tx)}")
|
||||
client.publishTransaction(tx)(singleThreadExecutionContext).recover {
|
||||
case t: Throwable if t.getMessage.contains("(code: -25)") && !isRetry => // we retry only once
|
||||
case t: Throwable if t.getMessage.contains("-25") && !isRetry => // we retry only once
|
||||
import akka.pattern.after
|
||||
|
||||
import scala.concurrent.duration._
|
||||
after(3 seconds, context.system.scheduler)(Future.successful({})).map(x => publish(tx, isRetry = true))
|
||||
case t: Throwable => log.error(s"cannot publish tx: reason=${t.getMessage} txid=${tx.txid} tx=$tx")
|
||||
}
|
||||
}
|
||||
|
||||
def checkConfirmed(w: WatchConfirmed) = {
|
||||
log.debug(s"checking confirmations of txid=${w.txId}")
|
||||
client.getTxConfirmations(w.txId.toString).map {
|
||||
case Some(confirmations) if confirmations >= w.minDepth =>
|
||||
client.getTransactionShortId(w.txId.toString).map {
|
||||
case (height, index) => self ! TriggerEvent(w, WatchEventConfirmed(w.event, height, index))
|
||||
}
|
||||
case t: Throwable => log.error(s"cannot publish tx: reason=${t.getMessage} txid=${tx.txid} tx=${BinaryData(Transaction.write(tx))}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,38 +206,4 @@ object ZmqWatcher {
|
||||
|
||||
case object TickNewBlock
|
||||
|
||||
def utxo(w: Watch): Option[OutPoint] =
|
||||
w match {
|
||||
case w: WatchSpent => Some(OutPoint(w.txId.reverse, w.outputIndex))
|
||||
case w: WatchSpentBasic => Some(OutPoint(w.txId.reverse, w.outputIndex))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
/**
|
||||
* The resulting map allows checking spent txes in constant time wrt number of watchers
|
||||
*
|
||||
* @param watches
|
||||
* @return
|
||||
*/
|
||||
def addWatchedUtxos(m: Map[OutPoint, Set[Watch]], w: Watch): Map[OutPoint, Set[Watch]] = {
|
||||
utxo(w) match {
|
||||
case Some(utxo) => m.get(utxo) match {
|
||||
case Some(watches) => m + (utxo -> (watches + w))
|
||||
case None => m + (utxo -> Set(w))
|
||||
}
|
||||
case None => m
|
||||
}
|
||||
}
|
||||
|
||||
def removeWatchedUtxos(m: Map[OutPoint, Set[Watch]], w: Watch): Map[OutPoint, Set[Watch]] = {
|
||||
utxo(w) match {
|
||||
case Some(utxo) => m.get(utxo) match {
|
||||
case Some(watches) if watches - w == Set.empty => m - utxo
|
||||
case Some(watches) => m + (utxo -> (watches - w))
|
||||
case None => m
|
||||
}
|
||||
case None => m
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,51 +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.blockchain.bitcoind.rpc
|
||||
|
||||
import com.softwaremill.sttp._
|
||||
import com.softwaremill.sttp.json4s._
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.json4s.jackson.Serialization
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class BasicBitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit http: SttpBackend[Future, Nothing]) extends BitcoinJsonRPCClient {
|
||||
|
||||
val scheme = if (ssl) "https" else "http"
|
||||
implicit val formats = DefaultFormats.withBigDecimal
|
||||
implicit val serialization = Serialization
|
||||
|
||||
override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] =
|
||||
invoke(Seq(JsonRPCRequest(method = method, params = params))).map(l => jsonResponse2Exception(l.head).result)
|
||||
|
||||
def jsonResponse2Exception(jsonRPCResponse: JsonRPCResponse): JsonRPCResponse = jsonRPCResponse match {
|
||||
case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
|
||||
case o => o
|
||||
}
|
||||
|
||||
def invoke(requests: Seq[JsonRPCRequest])(implicit ec: ExecutionContext): Future[Seq[JsonRPCResponse]] =
|
||||
for {
|
||||
res <- sttp
|
||||
.post(uri"$scheme://$host:$port")
|
||||
.body(requests)
|
||||
.auth.basic(user, password)
|
||||
.response(asJson[Seq[JsonRPCResponse]])
|
||||
.send()
|
||||
} yield res.unsafeBody
|
||||
|
||||
}
|
||||
@ -1,35 +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.blockchain.bitcoind.rpc
|
||||
|
||||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import org.json4s.JsonAST
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class BatchingBitcoinJsonRPCClient(rpcClient: BasicBitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends BitcoinJsonRPCClient {
|
||||
|
||||
implicit val timeout = Timeout(1 hour)
|
||||
|
||||
val batchingClient = system.actorOf(Props(new BatchingClient(rpcClient)), name = "batching-client")
|
||||
|
||||
override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JsonAST.JValue] =
|
||||
(batchingClient ? JsonRPCRequest(method = method, params = params)).mapTo[JsonAST.JValue]
|
||||
}
|
||||
@ -1,78 +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.blockchain.bitcoind.rpc
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Status}
|
||||
import akka.pattern.pipe
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BatchingClient.Pending
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
|
||||
class BatchingClient(rpcClient: BasicBitcoinJsonRPCClient) extends Actor with ActorLogging {
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
override def receive: Receive = {
|
||||
case request: JsonRPCRequest =>
|
||||
// immediately process isolated request
|
||||
process(queue = Queue(Pending(request, sender)))
|
||||
}
|
||||
|
||||
def waiting(queue: Queue[Pending], processing: Seq[Pending]): Receive = {
|
||||
case request: JsonRPCRequest =>
|
||||
// there is already a batch in flight, just add this request to the queue
|
||||
context become waiting(queue :+ Pending(request, sender), processing)
|
||||
|
||||
case responses: Seq[JsonRPCResponse]@unchecked =>
|
||||
log.debug(s"got {} responses", responses.size)
|
||||
// let's send back answers to the requestors
|
||||
require(responses.size == processing.size, s"responses=${responses.size} != processing=${processing.size}")
|
||||
responses.zip(processing).foreach {
|
||||
case (JsonRPCResponse(result, None, _), Pending(_, requestor)) => requestor ! result
|
||||
case (JsonRPCResponse(_, Some(error), _), Pending(_, requestor)) => requestor ! Status.Failure(JsonRPCError(error))
|
||||
}
|
||||
process(queue)
|
||||
|
||||
case s@Status.Failure(t) =>
|
||||
log.error(t, s"got exception for batch of ${processing.size} requests")
|
||||
// let's fail all requests
|
||||
processing.foreach { case Pending(_, requestor) => requestor ! s }
|
||||
process(queue)
|
||||
}
|
||||
|
||||
def process(queue: Queue[Pending]) = {
|
||||
// do we have queued requests?
|
||||
if (queue.isEmpty) {
|
||||
log.debug(s"no more requests, going back to idle")
|
||||
context become receive
|
||||
} else {
|
||||
val (batch, rest) = queue.splitAt(BatchingClient.BATCH_SIZE)
|
||||
log.debug(s"sending {} request(s): {} (queue={})", batch.size, batch.groupBy(_.request.method).map(e => e._1 + "=" + e._2.size).mkString(" "), queue.size)
|
||||
rpcClient.invoke(batch.map(_.request)) pipeTo self
|
||||
context become waiting(rest, batch)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BatchingClient {
|
||||
|
||||
val BATCH_SIZE = 50
|
||||
|
||||
case class Pending(request: JsonRPCRequest, requestor: ActorRef)
|
||||
|
||||
}
|
||||
@ -1,36 +1,81 @@
|
||||
/*
|
||||
* 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.blockchain.bitcoind.rpc
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.marshalling.Marshal
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.stream.{ActorMaterializer, OverflowStrategy, QueueOfferResult}
|
||||
import akka.stream.scaladsl.{Keep, Sink, Source}
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.json4s.{DefaultFormats, jackson}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
trait BitcoinJsonRPCClient {
|
||||
|
||||
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue]
|
||||
|
||||
}
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise}
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
// @formatter:off
|
||||
case class JsonRPCRequest(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[Any])
|
||||
case class Error(code: Int, message: String)
|
||||
case class JsonRPCResponse(result: JValue, error: Option[Error], id: String)
|
||||
case class JsonRPCError(error: Error) extends IOException(s"${error.message} (code: ${error.code})")
|
||||
// @formatter:on
|
||||
// @formatter:on
|
||||
|
||||
class BitcoinJsonRPCClient(user: String, password: String, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false)(implicit system: ActorSystem) {
|
||||
|
||||
val scheme = if (ssl) "https" else "http"
|
||||
val uri = Uri(s"$scheme://$host:$port")
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
implicit val materializer = ActorMaterializer()
|
||||
val httpClientFlow = Http().cachedHostConnectionPool[Promise[HttpResponse]](host, port)
|
||||
|
||||
val queueSize = 512
|
||||
val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](queueSize, OverflowStrategy.dropNew)
|
||||
.via(httpClientFlow)
|
||||
.toMat(Sink.foreach({
|
||||
case ((Success(resp), p)) => p.success(resp)
|
||||
case ((Failure(e), p)) => p.failure(e)
|
||||
}))(Keep.left)
|
||||
.run()
|
||||
|
||||
def queueRequest(request: HttpRequest): Future[HttpResponse] = {
|
||||
val responsePromise = Promise[HttpResponse]()
|
||||
queue.offer(request -> responsePromise).flatMap {
|
||||
case QueueOfferResult.Enqueued => responsePromise.future
|
||||
case QueueOfferResult.Dropped => Future.failed(new RuntimeException("Queue overflowed. Try again later."))
|
||||
case QueueOfferResult.Failure(ex) => Future.failed(ex)
|
||||
case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later."))
|
||||
}
|
||||
}
|
||||
|
||||
def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] =
|
||||
for {
|
||||
entity <- Marshal(JsonRPCRequest(method = method, params = params)).to[RequestEntity]
|
||||
httpRes <- queueRequest(HttpRequest(uri = "/", method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
|
||||
jsonRpcRes <- Unmarshal(httpRes).to[JsonRPCResponse].map {
|
||||
case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
|
||||
case o => o
|
||||
} recover {
|
||||
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
|
||||
}
|
||||
} yield jsonRpcRes.result
|
||||
|
||||
def invoke(request: Seq[(String, Seq[Any])])(implicit ec: ExecutionContext): Future[Seq[JValue]] =
|
||||
for {
|
||||
entity <- Marshal(request.map(r => JsonRPCRequest(method = r._1, params = r._2))).to[RequestEntity]
|
||||
httpRes <- queueRequest(HttpRequest(uri = "/", method = HttpMethods.POST).addHeader(Authorization(BasicHttpCredentials(user, password))).withEntity(entity))
|
||||
jsonRpcRes <- Unmarshal(httpRes).to[Seq[JsonRPCResponse]].map {
|
||||
//case JsonRPCResponse(_, Some(error), _) => throw JsonRPCError(error)
|
||||
case o => o
|
||||
} recover {
|
||||
case t: Throwable if httpRes.status == StatusCodes.Unauthorized => throw new RuntimeException("bitcoind replied with 401/Unauthorized (bad user/password?)", t)
|
||||
}
|
||||
} yield jsonRpcRes.map(_.result)
|
||||
|
||||
}
|
||||
@ -1,25 +1,8 @@
|
||||
/*
|
||||
* 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.blockchain.bitcoind.rpc
|
||||
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.ShortChannelId.coordinates
|
||||
import fr.acinq.eclair.TxCoordinates
|
||||
import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateResult}
|
||||
import fr.acinq.eclair.blockchain.{IndividualResult, ParallelGetResponse}
|
||||
import fr.acinq.eclair.fromShortId
|
||||
import fr.acinq.eclair.wire.ChannelAnnouncement
|
||||
import org.json4s.JsonAST._
|
||||
|
||||
@ -33,6 +16,13 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
|
||||
// TODO: this will probably not be needed once segwit is merged into core
|
||||
val protocolVersion = Protocol.PROTOCOL_VERSION
|
||||
|
||||
def tx2Hex(tx: Transaction): String = toHexString(Transaction.write(tx, protocolVersion))
|
||||
|
||||
def hex2tx(hex: String): Transaction = Transaction.read(hex, protocolVersion)
|
||||
|
||||
def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] =
|
||||
rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
|
||||
.map(json => Some((json \ "confirmations").extractOrElse[Int](0)))
|
||||
@ -47,18 +37,22 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
case t: JsonRPCError if t.error.code == -5 => None
|
||||
}
|
||||
|
||||
def lookForSpendingTx(blockhash_opt: Option[String], txid: String, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] =
|
||||
def getBlockHashesSinceBlockHash(blockHash: String, previous: Seq[String] = Nil)(implicit ec: ExecutionContext): Future[Seq[String]] =
|
||||
for {
|
||||
blockhash <- blockhash_opt match {
|
||||
case Some(b) => Future.successful(b)
|
||||
case None => rpcClient.invoke("getbestblockhash") collect { case JString(b) => b }
|
||||
nextblockhash_opt <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String]))
|
||||
res <- nextblockhash_opt match {
|
||||
case Some(nextBlockHash) => getBlockHashesSinceBlockHash(nextBlockHash, previous :+ nextBlockHash)
|
||||
case None => Future.successful(previous)
|
||||
}
|
||||
// with a verbosity of 0, getblock returns the raw serialized block
|
||||
block <- rpcClient.invoke("getblock", blockhash, 0).collect { case JString(b) => Block.read(b) }
|
||||
prevblockhash = block.header.hashPreviousBlock.reverse.toHex
|
||||
res <- block.tx.find(tx => tx.txIn.exists(i => i.outPoint.txid.toString() == txid && i.outPoint.index == outputIndex)) match {
|
||||
case None => lookForSpendingTx(Some(prevblockhash), txid, outputIndex)
|
||||
case Some(tx) => Future.successful(tx)
|
||||
} yield res
|
||||
|
||||
def getTxsSinceBlockHash(blockHash: String, previous: Seq[Transaction] = Nil)(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
|
||||
for {
|
||||
(nextblockhash_opt, txids) <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]]))
|
||||
next <- Future.sequence(txids.map(getTransaction(_)))
|
||||
res <- nextblockhash_opt match {
|
||||
case Some(nextBlockHash) => getTxsSinceBlockHash(nextBlockHash, previous ++ next)
|
||||
case None => Future.successful(previous ++ next)
|
||||
}
|
||||
} yield res
|
||||
|
||||
@ -68,6 +62,21 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
txs <- Future.sequence(txids.map(getTransaction(_)))
|
||||
} yield txs
|
||||
|
||||
/**
|
||||
* *used in interop test*
|
||||
* tell bitcoind to sent bitcoins from a specific local account
|
||||
*
|
||||
* @param account name of the local account to send bitcoins from
|
||||
* @param destination destination address
|
||||
* @param amount amount in BTC (not milliBTC, not Satoshis !!)
|
||||
* @param ec execution context
|
||||
* @return a Future[txid] where txid (a String) is the is of the tx that sends the bitcoins
|
||||
*/
|
||||
def sendFromAccount(account: String, destination: String, amount: Double)(implicit ec: ExecutionContext): Future[String] =
|
||||
rpcClient.invoke("sendfrom", account, destination, amount) collect {
|
||||
case JString(txid) => txid
|
||||
}
|
||||
|
||||
/**
|
||||
* @param txId
|
||||
* @param ec
|
||||
@ -81,11 +90,21 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] =
|
||||
getRawTransaction(txId).map(raw => Transaction.read(raw))
|
||||
|
||||
def isTransactionOutputSpendable(txId: String, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
|
||||
def getTransaction(height: Int, index: Int)(implicit ec: ExecutionContext): Future[Transaction] =
|
||||
for {
|
||||
json <- rpcClient.invoke("gettxout", txId, outputIndex, includeMempool)
|
||||
hash <- rpcClient.invoke("getblockhash", height).map(json => json.extract[String])
|
||||
json <- rpcClient.invoke("getblock", hash)
|
||||
JArray(txs) = json \ "tx"
|
||||
txid = txs(index).extract[String]
|
||||
tx <- getTransaction(txid)
|
||||
} yield tx
|
||||
|
||||
def isTransactionOuputSpendable(txId: String, ouputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
|
||||
for {
|
||||
json <- rpcClient.invoke("gettxout", txId, ouputIndex, includeMempool)
|
||||
} yield json != JNull
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param txId transaction id
|
||||
@ -106,28 +125,14 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
future
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a transaction on the bitcoin network.
|
||||
*
|
||||
* Note that this method is idempotent, meaning that if the tx was already published a long time ago, then this is
|
||||
* considered a success even if bitcoin core rejects this new attempt.
|
||||
*
|
||||
* @param tx
|
||||
* @param ec
|
||||
* @return
|
||||
*/
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
|
||||
rpcClient.invoke("sendrawtransaction", tx.toString()) collect {
|
||||
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
|
||||
rpcClient.invoke("sendrawtransaction", hex) collect {
|
||||
case JString(txid) => txid
|
||||
} recoverWith {
|
||||
case JsonRPCError(Error(-27, _)) =>
|
||||
// "transaction already in block chain (code: -27)" ignore error
|
||||
Future.successful(tx.txid.toString())
|
||||
case e@JsonRPCError(Error(-25, _)) =>
|
||||
// "missing inputs (code: -25)" it may be that the tx has already been published and its output spent
|
||||
getRawTransaction(tx.txid.toString()).map { case _ => tx.txid.toString() }.recoverWith { case _ => Future.failed[String](e) }
|
||||
}
|
||||
|
||||
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
|
||||
publishTransaction(tx2Hex(tx))
|
||||
|
||||
/**
|
||||
* We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent
|
||||
* to time.now())
|
||||
@ -140,29 +145,51 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
|
||||
case JInt(count) => count.toLong
|
||||
}
|
||||
|
||||
def validate(c: ChannelAnnouncement)(implicit ec: ExecutionContext): Future[ValidateResult] = {
|
||||
val TxCoordinates(blockHeight, txIndex, outputIndex) = coordinates(c.shortChannelId)
|
||||
def getParallel(awaiting: Seq[ChannelAnnouncement]): Future[ParallelGetResponse] = {
|
||||
case class TxCoordinate(blockHeight: Int, txIndex: Int, outputIndex: Int)
|
||||
|
||||
val coordinates = awaiting.map {
|
||||
case c =>
|
||||
val (blockHeight, txIndex, outputIndex) = fromShortId(c.shortChannelId)
|
||||
TxCoordinate(blockHeight, txIndex, outputIndex)
|
||||
}.zipWithIndex
|
||||
|
||||
import ExecutionContext.Implicits.global
|
||||
implicit val formats = org.json4s.DefaultFormats
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
} yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus)))
|
||||
|
||||
} recover { case t: Throwable => ValidateResult(c, Left(t)) }
|
||||
blockHashes: Seq[String] <- rpcClient.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32)))
|
||||
txids: Seq[String] <- rpcClient.invoke(blockHashes.map(h => ("getblock", h :: Nil)))
|
||||
.map(_.zipWithIndex)
|
||||
.map(_.map {
|
||||
case (json, idx) => Try {
|
||||
val JArray(txs) = json \ "tx"
|
||||
txs(coordinates(idx)._1.txIndex).extract[String]
|
||||
} getOrElse ("00" * 32)
|
||||
})
|
||||
txs <- rpcClient.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map {
|
||||
case JString(raw) => Some(Transaction.read(raw))
|
||||
case _ => None
|
||||
})
|
||||
unspent <- rpcClient.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull))
|
||||
} yield ParallelGetResponse(awaiting.zip(txs.zip(unspent)).map(x => IndividualResult(x._1, x._2._1, x._2._2)))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*object Test extends App {
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import ExecutionContext.Implicits.global
|
||||
implicit val system = ActorSystem()
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
|
||||
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = "foo",
|
||||
password = "bar",
|
||||
host = "localhost",
|
||||
port = 28332))
|
||||
|
||||
println(Await.result(bitcoin_client.getTxBlockHash("dcb0abfa822402ce379fedd7bbbb2c824e53ef300313594c39282da1efd35f17"), 10 seconds))
|
||||
}*/
|
||||
|
||||
@ -1,86 +1,61 @@
|
||||
/*
|
||||
* 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.blockchain.bitcoind.zmq
|
||||
|
||||
import akka.Done
|
||||
import akka.actor.{Actor, ActorLogging}
|
||||
import fr.acinq.bitcoin.{Block, Transaction}
|
||||
import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction}
|
||||
import org.zeromq.ZMQ.Event
|
||||
import org.zeromq.{SocketType, ZContext, ZMQ, ZMsg}
|
||||
import org.zeromq.{ZContext, ZMQ, ZMsg}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.Promise
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Promise}
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Created by PM on 04/04/2017.
|
||||
*/
|
||||
class ZMQActor(address: String, connected: Option[Promise[Done]] = None) extends Actor with ActorLogging {
|
||||
class ZMQActor(address: String, connected: Option[Promise[Boolean]] = None) extends Actor with ActorLogging {
|
||||
|
||||
import ZMQActor._
|
||||
|
||||
val ctx = new ZContext
|
||||
|
||||
val subscriber = ctx.createSocket(SocketType.SUB)
|
||||
val subscriber = ctx.createSocket(ZMQ.SUB)
|
||||
subscriber.monitor("inproc://events", ZMQ.EVENT_CONNECTED | ZMQ.EVENT_DISCONNECTED)
|
||||
subscriber.connect(address)
|
||||
subscriber.subscribe("rawblock".getBytes(ZMQ.CHARSET))
|
||||
subscriber.subscribe("rawtx".getBytes(ZMQ.CHARSET))
|
||||
|
||||
val monitor = ctx.createSocket(SocketType.PAIR)
|
||||
val monitor = ctx.createSocket(ZMQ.PAIR)
|
||||
monitor.connect("inproc://events")
|
||||
|
||||
implicit val ec: ExecutionContext = context.system.dispatcher
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
// we check messages in a non-blocking manner with an interval, making sure to retrieve all messages before waiting again
|
||||
@tailrec
|
||||
final def checkEvent: Unit = Option(Event.recv(monitor, ZMQ.DONTWAIT)) match {
|
||||
def checkEvent: Unit = Option(Event.recv(monitor, ZMQ.DONTWAIT)) match {
|
||||
case Some(event) =>
|
||||
self ! event
|
||||
checkEvent
|
||||
case None => ()
|
||||
case None =>
|
||||
context.system.scheduler.scheduleOnce(1 second)(checkEvent)
|
||||
}
|
||||
|
||||
@tailrec
|
||||
final def checkMsg: Unit = Option(ZMsg.recvMsg(subscriber, ZMQ.DONTWAIT)) match {
|
||||
def checkMsg: Unit = Option(ZMsg.recvMsg(subscriber, ZMQ.DONTWAIT)) match {
|
||||
case Some(msg) =>
|
||||
self ! msg
|
||||
checkMsg
|
||||
case None => ()
|
||||
case None =>
|
||||
context.system.scheduler.scheduleOnce(1 second)(checkMsg)
|
||||
}
|
||||
|
||||
self ! 'checkEvent
|
||||
self ! 'checkMsg
|
||||
checkEvent
|
||||
checkMsg
|
||||
|
||||
override def receive: Receive = {
|
||||
case 'checkEvent =>
|
||||
checkEvent
|
||||
context.system.scheduler.scheduleOnce(1 second, self ,'checkEvent)
|
||||
|
||||
case 'checkMsg =>
|
||||
checkMsg
|
||||
context.system.scheduler.scheduleOnce(1 second, self, 'checkMsg)
|
||||
|
||||
case event: Event => event.getEvent match {
|
||||
case ZMQ.EVENT_CONNECTED =>
|
||||
log.info(s"connected to ${event.getAddress}")
|
||||
Try(connected.map(_.success(Done)))
|
||||
Try(connected.map(_.success(true)))
|
||||
context.system.eventStream.publish(ZMQConnected)
|
||||
case ZMQ.EVENT_DISCONNECTED =>
|
||||
log.warning(s"disconnected from ${event.getAddress}")
|
||||
@ -91,11 +66,11 @@ class ZMQActor(address: String, connected: Option[Promise[Done]] = None) extends
|
||||
case msg: ZMsg => msg.popString() match {
|
||||
case "rawblock" =>
|
||||
val block = Block.read(msg.pop().getData)
|
||||
log.debug("received blockid={}", block.blockId)
|
||||
log.debug(s"received blockid=${block.blockId}")
|
||||
context.system.eventStream.publish(NewBlock(block))
|
||||
case "rawtx" =>
|
||||
val tx = Transaction.read(msg.pop().getData)
|
||||
log.debug("received txid={}", tx.txid)
|
||||
log.debug(s"received txid=${tx.txid}")
|
||||
context.system.eventStream.publish(NewTransaction(tx))
|
||||
case topic => log.warning(s"unexpected topic=$topic")
|
||||
}
|
||||
|
||||
@ -0,0 +1,152 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Transaction
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockCount
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjKit._
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
|
||||
import org.bitcoinj.core.listeners._
|
||||
import org.bitcoinj.core.{Block, Context, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, VersionMessage, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.params.{RegTestParams, TestNet3Params}
|
||||
import org.bitcoinj.utils.Threading
|
||||
import org.bitcoinj.wallet.Wallet
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class BitcoinjKit(chain: String, datadir: File, staticPeers: List[InetSocketAddress] = Nil)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj", true) with Logging {
|
||||
|
||||
if (staticPeers.size > 0) {
|
||||
logger.info(s"using staticPeers=${staticPeers.mkString(",")}")
|
||||
setPeerNodes(staticPeers.map(addr => new PeerAddress(params, addr)).head)
|
||||
}
|
||||
|
||||
// tells us when the peerGroup/chain/wallet are accessible
|
||||
private val initializedPromise = Promise[Boolean]()
|
||||
val initialized = initializedPromise.future
|
||||
|
||||
// tells us as soon as we know the current block height
|
||||
private val atCurrentHeightPromise = Promise[Boolean]()
|
||||
val atCurrentHeight = atCurrentHeightPromise.future
|
||||
|
||||
// tells us when we are at current block height
|
||||
// private val syncedPromise = Promise[Boolean]()
|
||||
// val synced = syncedPromise.future
|
||||
|
||||
private def updateBlockCount(blockCount: Int) = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
logger.debug(s"current blockchain height=$blockCount")
|
||||
system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
}
|
||||
|
||||
override def onSetupCompleted(): Unit = {
|
||||
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
|
||||
peerGroup().setMinRequiredProtocolVersion(70015) // bitcoin core 0.13
|
||||
wallet().watchMode = true
|
||||
|
||||
// setDownloadListener(new DownloadProgressTracker {
|
||||
// override def doneDownload(): Unit = {
|
||||
// super.doneDownload()
|
||||
// // may be called multiple times
|
||||
// syncedPromise.trySuccess(true)
|
||||
// }
|
||||
// })
|
||||
|
||||
// we set the blockcount to the previous stored block height
|
||||
updateBlockCount(chain().getBestChainHeight)
|
||||
|
||||
// as soon as we are connected the peers will tell us their current height and we will advertise it immediately
|
||||
peerGroup().addConnectedEventListener(new PeerConnectedEventListener {
|
||||
override def onPeerConnected(peer: Peer, peerCount: Int): Unit = {
|
||||
if ((peer.getPeerVersionMessage.localServices & VersionMessage.NODE_WITNESS) == 0) {
|
||||
peer.close()
|
||||
} else {
|
||||
Context.propagate(wallet.getContext)
|
||||
// we wait for at least 3 peers before relying on the information they are giving, but we trust localhost
|
||||
if (peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) {
|
||||
updateBlockCount(peerGroup().getMostCommonChainHeight)
|
||||
// may be called multiple times
|
||||
atCurrentHeightPromise.trySuccess(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener(new BlocksDownloadedEventListener {
|
||||
override def onBlocksDownloaded(peer: Peer, block: Block, filteredBlock: FilteredBlock, blocksLeft: Int): Unit = {
|
||||
Context.propagate(wallet.getContext)
|
||||
logger.debug(s"received block=${block.getHashAsString} (size=${block.bitcoinSerialize().size} txs=${Try(block.getTransactions.size).getOrElse(-1)}) filteredBlock=${Try(filteredBlock.getHash.toString).getOrElse("N/A")} (size=${Try(block.bitcoinSerialize().size).getOrElse(-1)} txs=${Try(filteredBlock.getTransactionCount).getOrElse(-1)})")
|
||||
Try {
|
||||
if (filteredBlock.getAssociatedTransactions.size() > 0) {
|
||||
logger.info(s"retrieving full block ${block.getHashAsString}")
|
||||
Futures.addCallback(peer.getBlock(block.getHash), new FutureCallback[Block] {
|
||||
override def onFailure(throwable: Throwable) = logger.error(s"could not retrieve full block=${block.getHashAsString}")
|
||||
|
||||
override def onSuccess(fullBlock: Block) = {
|
||||
Try {
|
||||
Context.propagate(wallet.getContext)
|
||||
fullBlock.getTransactions.foreach {
|
||||
case tx =>
|
||||
logger.debug(s"received tx=${tx.getHashAsString} witness=${Transaction.read(tx.bitcoinSerialize()).txIn(0).witness.stack.size} from fullBlock=${fullBlock.getHash} confidence=${tx.getConfidence}")
|
||||
val depthInBlocks = tx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => -1
|
||||
case _ => tx.getConfidence.getDepthInBlocks
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(Transaction.read(tx.bitcoinSerialize()), 0, depthInBlocks))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Threading.USER_THREAD)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
chain().addNewBestBlockListener(new NewBestBlockListener {
|
||||
override def notifyNewBestBlock(storedBlock: StoredBlock): Unit =
|
||||
updateBlockCount(storedBlock.getHeight)
|
||||
})
|
||||
|
||||
wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener {
|
||||
override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = {
|
||||
Context.propagate(wallet.getContext)
|
||||
val tx = Transaction.read(bitcoinjTx.bitcoinSerialize())
|
||||
logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence} witness=${bitcoinjTx.getWitness(0)}")
|
||||
val (blockHeight, confirmations) = bitcoinjTx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => (-1, -1)
|
||||
case ConfidenceType.BUILDING => (bitcoinjTx.getConfidence.getAppearedAtChainHeight, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
case _ => (-1, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(tx, blockHeight, confirmations))
|
||||
}
|
||||
})
|
||||
|
||||
initializedPromise.success(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjKit {
|
||||
|
||||
def chain2Params(chain: String): NetworkParameters = chain match {
|
||||
case "regtest" => RegTestParams.get()
|
||||
case "test" => TestNet3Params.get()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.{Coin, Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.wallet.{SendRequest, Wallet}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 08/07/2017.
|
||||
*/
|
||||
class BitcoinjWallet(val fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions())
|
||||
|
||||
override def getBalance: Future[Satoshi] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
Context.propagate(wallet.getContext)
|
||||
Satoshi(wallet.getBalance.longValue())
|
||||
}
|
||||
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
Context.propagate(wallet.getContext)
|
||||
wallet.currentReceiveAddress().toBase58
|
||||
}
|
||||
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
logger.info(s"building funding tx")
|
||||
Context.propagate(wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
val tx = new BitcoinjTransaction(wallet.getParams)
|
||||
tx.addOutput(Coin.valueOf(amount.amount), script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
val txOutputIndex = tx.getOutputs.find(_.getScriptPubKey.equals(script)).get.getIndex
|
||||
MakeFundingTxResponse(Transaction.read(tx.bitcoinSerialize()), txOutputIndex)
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = {
|
||||
// we make sure that we haven't double spent our own tx (eg by opening 2 channels at the same time)
|
||||
val serializedTx = Transaction.write(tx)
|
||||
logger.info(s"committing tx: txid=${tx.txid} tx=$serializedTx")
|
||||
for {
|
||||
wallet <- fWallet
|
||||
_ = Context.propagate(wallet.getContext)
|
||||
bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx)
|
||||
canCommit = wallet.maybeCommitTx(bitcoinjTx)
|
||||
_ = logger.info(s"commit txid=${tx.txid} result=$canCommit")
|
||||
} yield canCommit
|
||||
}
|
||||
|
||||
/**
|
||||
* There are no locks on bitcoinj, this is a no-op
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
override def rollback(tx: Transaction) = Future.successful(true)
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props, Terminated}
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Script.{pay2wsh, write}
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.{Globals, fromShortId}
|
||||
import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.script.Script
|
||||
|
||||
import scala.collection.SortedMap
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
final case class NewConfidenceLevel(tx: Transaction, blockHeight: Int, confirmations: Int) extends BlockchainEvent
|
||||
|
||||
/**
|
||||
* A blockchain watcher that:
|
||||
* - receives bitcoin events (new blocks and new txes) directly from the bitcoin network
|
||||
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
|
||||
* Created by PM on 21/02/2016.
|
||||
*/
|
||||
class BitcoinjWatcher(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel])
|
||||
|
||||
val broadcaster = context.actorOf(Props(new Broadcaster(kit: WalletAppKit)), name = "broadcaster")
|
||||
|
||||
case class TriggerEvent(w: Watch, e: WatchEvent)
|
||||
|
||||
def receive: Receive = watching(Set(), SortedMap(), Nil, Nil)
|
||||
|
||||
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = {
|
||||
|
||||
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
|
||||
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=${Transaction.write(tx)}")
|
||||
watches.collect {
|
||||
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(event))
|
||||
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpent(event, tx))
|
||||
case w@WatchConfirmed(_, txId, _, minDepth, event) if txId == tx.txid && confirmations >= minDepth =>
|
||||
self ! TriggerEvent(w, WatchEventConfirmed(event, blockHeight, 0))
|
||||
}
|
||||
context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event, sent)
|
||||
|
||||
case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) =>
|
||||
log.info(s"triggering $w")
|
||||
w.channel ! e
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches
|
||||
context.become(watching(newWatches, block2tx, oldEvents, sent :+ t))
|
||||
|
||||
case CurrentBlockCount(count) => {
|
||||
val toPublish = block2tx.filterKeys(_ <= count)
|
||||
toPublish.values.flatten.map(tx => publish(tx))
|
||||
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent))
|
||||
}
|
||||
|
||||
case w: Watch if !watches.contains(w) =>
|
||||
w match {
|
||||
case w: WatchConfirmed => addHint(w.publicKeyScript)
|
||||
case w: WatchSpent => addHint(w.publicKeyScript)
|
||||
case w: WatchSpentBasic => addHint(w.publicKeyScript)
|
||||
case _ => ()
|
||||
}
|
||||
log.debug(s"adding watch $w for $sender")
|
||||
log.info(s"resending ${oldEvents.size} events!")
|
||||
oldEvents.foreach(self ! _)
|
||||
context.watch(w.channel)
|
||||
context.become(watching(watches + w, block2tx, oldEvents, sent))
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val cltvTimeout = Scripts.cltvTimeout(tx)
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
|
||||
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
|
||||
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
|
||||
} else if (cltvTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
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 csvTimeout = Scripts.csvTimeout(tx)
|
||||
val absTimeout = blockHeight + csvTimeout
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
|
||||
case c =>
|
||||
log.info(s"blindly validating channel=$c")
|
||||
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
|
||||
val (_, _, outputIndex) = fromShortId(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)
|
||||
IndividualResult(c, Some(fakeFundingTx), true)
|
||||
})
|
||||
|
||||
case Terminated(channel) =>
|
||||
// we remove watches associated to dead actor
|
||||
val deprecatedWatches = watches.filter(_.channel == channel)
|
||||
context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents, sent))
|
||||
|
||||
case 'watches => sender ! watches
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitcoinj needs hints to be able to detect transactions
|
||||
*
|
||||
* @param pubkeyScript
|
||||
* @return
|
||||
*/
|
||||
def addHint(pubkeyScript: BinaryData) = {
|
||||
Context.propagate(kit.wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
// set creation time to 2017/09/01, so bitcoinj can still use its checkpoints optimizations
|
||||
script.setCreationTimeSeconds(1501538400L) // 2017-09-01
|
||||
kit.wallet().addWatchedScripts(ImmutableList.of(script))
|
||||
}
|
||||
|
||||
def publish(tx: Transaction): Unit = broadcaster ! tx
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjWatcher {
|
||||
|
||||
def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new BitcoinjWatcher(kit)(ec))
|
||||
|
||||
}
|
||||
|
||||
class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging {
|
||||
|
||||
override def receive: Receive = {
|
||||
case tx: Transaction =>
|
||||
broadcast(tx)
|
||||
context become waiting(Nil)
|
||||
}
|
||||
|
||||
def waiting(stash: Seq[Transaction]): Receive = {
|
||||
case BroadcastResult(tx, result) =>
|
||||
result match {
|
||||
case Success(_) => log.info(s"broadcast success for txid=${tx.txid}")
|
||||
case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ")
|
||||
}
|
||||
stash match {
|
||||
case head :: rest =>
|
||||
broadcast(head)
|
||||
context become waiting(rest)
|
||||
case Nil => context become receive
|
||||
}
|
||||
case tx: Transaction =>
|
||||
log.info(s"stashing txid=${tx.txid} for broadcast")
|
||||
context become waiting(stash :+ tx)
|
||||
}
|
||||
|
||||
case class BroadcastResult(tx: Transaction, result: Try[Boolean])
|
||||
|
||||
def broadcast(tx: Transaction) = {
|
||||
Context.propagate(kit.wallet().getContext)
|
||||
val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx))
|
||||
log.info(s"broadcasting txid=${tx.txid}")
|
||||
Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] {
|
||||
override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t))
|
||||
|
||||
override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true))
|
||||
}, context.dispatcher)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,352 +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.blockchain.electrum
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, decodeCompact}
|
||||
import fr.acinq.eclair.blockchain.electrum.db.HeaderDb
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
case class Blockchain(chainHash: ByteVector32,
|
||||
checkpoints: Vector[CheckPoint],
|
||||
headersMap: Map[ByteVector32, Blockchain.BlockIndex],
|
||||
bestchain: Vector[Blockchain.BlockIndex],
|
||||
orphans: Map[ByteVector32, BlockHeader] = Map()) {
|
||||
|
||||
import Blockchain._
|
||||
|
||||
require(chainHash == Block.LivenetGenesisBlock.hash || chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash, s"invalid chain hash $chainHash")
|
||||
|
||||
def tip = bestchain.last
|
||||
|
||||
def height = if (bestchain.isEmpty) 0 else bestchain.last.height
|
||||
|
||||
/**
|
||||
* Build a chain of block indexes
|
||||
*
|
||||
* This is used in case of reorg to rebuilt the new best chain
|
||||
*
|
||||
* @param index last index of the chain
|
||||
* @param acc accumulator
|
||||
* @return the chain that starts at the genesis block and ends at index
|
||||
*/
|
||||
@tailrec
|
||||
private def buildChain(index: BlockIndex, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]): Vector[BlockIndex] = {
|
||||
index.parent match {
|
||||
case None => index +: acc
|
||||
case Some(parent) => buildChain(parent, index +: acc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param height block height
|
||||
* @return the encoded difficulty that a block at this height should have
|
||||
*/
|
||||
def getDifficulty(height: Int): Option[Long] = height match {
|
||||
case value if value < RETARGETING_PERIOD * (checkpoints.length + 1) =>
|
||||
// we're within our checkpoints
|
||||
val checkpoint = checkpoints(height / RETARGETING_PERIOD - 1)
|
||||
Some(checkpoint.nextBits)
|
||||
case value if value % RETARGETING_PERIOD != 0 =>
|
||||
// we're not at a retargeting height, difficulty is the same as for the previous block
|
||||
getHeader(height - 1).map(_.bits)
|
||||
case _ =>
|
||||
// difficulty retargeting
|
||||
for {
|
||||
previous <- getHeader(height - 1)
|
||||
firstBlock <- getHeader(height - RETARGETING_PERIOD)
|
||||
} yield BlockHeader.calculateNextWorkRequired(previous, firstBlock.time)
|
||||
}
|
||||
|
||||
def getHeader(height: Int): Option[BlockHeader] = if (!bestchain.isEmpty && height >= bestchain.head.height && height - bestchain.head.height < bestchain.size)
|
||||
Some(bestchain(height - bestchain.head.height).header)
|
||||
else None
|
||||
}
|
||||
|
||||
object Blockchain extends Logging {
|
||||
|
||||
val RETARGETING_PERIOD = 2016 // on bitcoin, the difficulty re-targeting period is 2016 blocks
|
||||
val MAX_REORG = 500 // we assume that there won't be a reorg of more than 500 blocks
|
||||
|
||||
/**
|
||||
*
|
||||
* @param header block header
|
||||
* @param height block height
|
||||
* @param parent parent block
|
||||
* @param chainwork cumulative chain work up to and including this block
|
||||
*/
|
||||
case class BlockIndex(header: BlockHeader, height: Int, parent: Option[BlockIndex], chainwork: BigInt) {
|
||||
lazy val hash = header.hash
|
||||
|
||||
lazy val blockId = header.blockId
|
||||
|
||||
lazy val logwork = if (chainwork == 0) 0.0 else Math.log(chainwork.doubleValue()) / Math.log(2.0)
|
||||
|
||||
override def toString = s"BlockIndex($blockId, $height, ${parent.map(_.blockId)}, $logwork)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an empty blockchain from a series of checkpoints
|
||||
*
|
||||
* @param chainhash chain we're on
|
||||
* @param checkpoints list of checkpoints
|
||||
* @return a blockchain instance
|
||||
*/
|
||||
def fromCheckpoints(chainhash: ByteVector32, checkpoints: Vector[CheckPoint]): Blockchain = {
|
||||
Blockchain(chainhash, checkpoints, Map(), Vector())
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in tests
|
||||
*/
|
||||
def fromGenesisBlock(chainhash: ByteVector32, genesis: BlockHeader): Blockchain = {
|
||||
require(chainhash == Block.RegtestGenesisBlock.hash)
|
||||
// the height of the genesis block is 0
|
||||
val blockIndex = BlockIndex(genesis, 0, None, decodeCompact(genesis.bits)._1)
|
||||
Blockchain(chainhash, Vector(), Map(blockIndex.hash -> blockIndex), Vector(blockIndex))
|
||||
}
|
||||
|
||||
/**
|
||||
* load an em
|
||||
*
|
||||
* @param chainHash
|
||||
* @param headerDb
|
||||
* @return
|
||||
*/
|
||||
def load(chainHash: ByteVector32, headerDb: HeaderDb): Blockchain = {
|
||||
val checkpoints = CheckPoint.load(chainHash)
|
||||
val checkpoints1 = headerDb.getTip match {
|
||||
case Some((height, header)) =>
|
||||
val newcheckpoints = for {h <- checkpoints.size * RETARGETING_PERIOD - 1 + RETARGETING_PERIOD to height - RETARGETING_PERIOD by RETARGETING_PERIOD} yield {
|
||||
val cpheader = headerDb.getHeader(h).get
|
||||
val nextDiff = headerDb.getHeader(h + 1).get.bits
|
||||
CheckPoint(cpheader.hash, nextDiff)
|
||||
}
|
||||
checkpoints ++ newcheckpoints
|
||||
case None => checkpoints
|
||||
}
|
||||
Blockchain.fromCheckpoints(chainHash, checkpoints1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a chunk of 2016 headers
|
||||
*
|
||||
* Used during initial sync to batch validate
|
||||
*
|
||||
* @param height height of the first header; must be a multiple of 2016
|
||||
* @param headers headers.
|
||||
* @throws Exception if this chunk is not valid and consistent with our checkpoints
|
||||
*/
|
||||
def validateHeadersChunk(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Unit = {
|
||||
if (headers.isEmpty) return
|
||||
|
||||
require(height % RETARGETING_PERIOD == 0, s"header chunk height $height not a multiple of 2016")
|
||||
require(BlockHeader.checkProofOfWork(headers.head))
|
||||
headers.tail.foldLeft(headers.head) {
|
||||
case (previous, current) =>
|
||||
require(BlockHeader.checkProofOfWork(current))
|
||||
require(current.hashPreviousBlock == previous.hash)
|
||||
// on mainnet all blocks with a re-targeting window have the same difficulty target
|
||||
// on testnet it doesn't hold, there can be a drop in difficulty if there are no blocks for 20 minutes
|
||||
blockchain.chainHash match {
|
||||
case Block.LivenetGenesisBlock | Block.RegtestGenesisBlock.hash => require(current.bits == previous.bits)
|
||||
case _ => ()
|
||||
}
|
||||
current
|
||||
}
|
||||
|
||||
val cpindex = (height / RETARGETING_PERIOD) - 1
|
||||
if (cpindex < blockchain.checkpoints.length) {
|
||||
// check that the first header in the chunk matches our checkpoint
|
||||
val checkpoint = blockchain.checkpoints(cpindex)
|
||||
require(headers(0).hashPreviousBlock == checkpoint.hash)
|
||||
blockchain.chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash => require(headers(0).bits == checkpoint.nextBits)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a checkpoint after this chunk, check that it is also satisfied
|
||||
if (cpindex < blockchain.checkpoints.length - 1) {
|
||||
require(headers.length == RETARGETING_PERIOD)
|
||||
val nextCheckpoint = blockchain.checkpoints(cpindex + 1)
|
||||
require(headers.last.hash == nextCheckpoint.hash)
|
||||
blockchain.chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash =>
|
||||
val diff = BlockHeader.calculateNextWorkRequired(headers.last, headers.head.time)
|
||||
require(diff == nextCheckpoint.nextBits)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addHeadersChunk(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Blockchain = {
|
||||
if (headers.length > RETARGETING_PERIOD) {
|
||||
val blockchain1 = Blockchain.addHeadersChunk(blockchain, height, headers.take(RETARGETING_PERIOD))
|
||||
return Blockchain.addHeadersChunk(blockchain1, height + RETARGETING_PERIOD, headers.drop(RETARGETING_PERIOD))
|
||||
}
|
||||
if (headers.isEmpty) return blockchain
|
||||
validateHeadersChunk(blockchain, height, headers)
|
||||
|
||||
height match {
|
||||
case _ if height == blockchain.checkpoints.length * RETARGETING_PERIOD =>
|
||||
// append after our last checkpoint
|
||||
|
||||
// checkpoints are (block hash, * next * difficulty target), this is why:
|
||||
// - we duplicate the first checkpoints because all headers in the first chunks on mainnet had the same difficulty target
|
||||
// - we drop the last checkpoint
|
||||
val chainwork = (blockchain.checkpoints(0) +: blockchain.checkpoints.dropRight(1)).map(t => BigInt(RETARGETING_PERIOD) * Blockchain.chainWork(t.nextBits)).sum
|
||||
val blockIndex = BlockIndex(headers.head, height, None, chainwork + Blockchain.chainWork(headers.head))
|
||||
val bestchain1 = headers.tail.foldLeft(Vector(blockIndex)) {
|
||||
case (indexes, header) => indexes :+ BlockIndex(header, indexes.last.height + 1, Some(indexes.last), indexes.last.chainwork + Blockchain.chainWork(header))
|
||||
}
|
||||
val headersMap1 = blockchain.headersMap ++ bestchain1.map(bi => bi.hash -> bi)
|
||||
blockchain.copy(bestchain = bestchain1, headersMap = headersMap1)
|
||||
case _ if height < blockchain.checkpoints.length * RETARGETING_PERIOD =>
|
||||
blockchain
|
||||
case _ if height == blockchain.height + 1 =>
|
||||
// attach at our best chain
|
||||
require(headers.head.hashPreviousBlock == blockchain.bestchain.last.hash)
|
||||
val blockIndex = BlockIndex(headers.head, height, None, blockchain.bestchain.last.chainwork + Blockchain.chainWork(headers.head))
|
||||
val indexes = headers.tail.foldLeft(Vector(blockIndex)) {
|
||||
case (indexes, header) => indexes :+ BlockIndex(header, indexes.last.height + 1, Some(indexes.last), indexes.last.chainwork + Blockchain.chainWork(header))
|
||||
}
|
||||
val bestchain1 = blockchain.bestchain ++ indexes
|
||||
val headersMap1 = blockchain.headersMap ++ indexes.map(bi => bi.hash -> bi)
|
||||
blockchain.copy(bestchain = bestchain1, headersMap = headersMap1)
|
||||
// do nothing; headers have been validated
|
||||
case _ => throw new IllegalArgumentException(s"cannot add headers chunk to an empty blockchain: not within our checkpoint")
|
||||
}
|
||||
}
|
||||
|
||||
def addHeader(blockchain: Blockchain, height: Int, header: BlockHeader): Blockchain = {
|
||||
require(BlockHeader.checkProofOfWork(header), s"invalid proof of work for $header")
|
||||
blockchain.headersMap.get(header.hashPreviousBlock) match {
|
||||
case Some(parent) if parent.height == height - 1 =>
|
||||
if (height % RETARGETING_PERIOD != 0 && (blockchain.chainHash == Block.LivenetGenesisBlock.hash || blockchain.chainHash == Block.RegtestGenesisBlock.hash)) {
|
||||
// check difficulty target, which should be the same as for the parent block
|
||||
// we only check this on mainnet, on testnet rules are much more lax
|
||||
require(header.bits == parent.header.bits, s"header invalid difficulty target for ${header}, it should be ${parent.header.bits}")
|
||||
}
|
||||
val blockIndex = BlockIndex(header, height, Some(parent), parent.chainwork + Blockchain.chainWork(header))
|
||||
val headersMap1 = blockchain.headersMap + (blockIndex.hash -> blockIndex)
|
||||
val bestChain1 = if (parent == blockchain.bestchain.last) {
|
||||
// simplest case: we add to our current best chain
|
||||
logger.info(s"new tip at $blockIndex")
|
||||
blockchain.bestchain :+ blockIndex
|
||||
} else if (blockIndex.chainwork > blockchain.bestchain.last.chainwork) {
|
||||
logger.info(s"new best chain at $blockIndex")
|
||||
// we have a new best chain
|
||||
buildChain(blockIndex)
|
||||
} else {
|
||||
logger.info(s"received header $blockIndex which is not on the best chain")
|
||||
blockchain.bestchain
|
||||
}
|
||||
blockchain.copy(headersMap = headersMap1, bestchain = bestChain1)
|
||||
case Some(parent) => throw new IllegalArgumentException(s"parent for $header at $height is not valid: $parent ")
|
||||
case None if height < blockchain.height - 1000 => blockchain
|
||||
case None => throw new IllegalArgumentException(s"cannot find parent for $header at $height")
|
||||
}
|
||||
}
|
||||
|
||||
def addHeaders(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Blockchain = {
|
||||
if (headers.isEmpty) blockchain
|
||||
else if (height % RETARGETING_PERIOD == 0) addHeadersChunk(blockchain, height, headers)
|
||||
else {
|
||||
@tailrec
|
||||
def loop(bc: Blockchain, h: Int, hs: Seq[BlockHeader]): Blockchain = if (hs.isEmpty) bc else {
|
||||
loop(Blockchain.addHeader(bc, h, hs.head), h + 1, hs.tail)
|
||||
}
|
||||
|
||||
loop(blockchain, height, headers)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* build a chain of block indexes
|
||||
*
|
||||
* @param index last index of the chain
|
||||
* @param acc accumulator
|
||||
* @return the chain that starts at the genesis block and ends at index
|
||||
*/
|
||||
@tailrec
|
||||
def buildChain(index: BlockIndex, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]): Vector[BlockIndex] = {
|
||||
index.parent match {
|
||||
case None => index +: acc
|
||||
case Some(parent) => buildChain(parent, index +: acc)
|
||||
}
|
||||
}
|
||||
|
||||
def chainWork(target: BigInt): BigInt = BigInt(2).pow(256) / (target + BigInt(1))
|
||||
|
||||
def chainWork(bits: Long): BigInt = {
|
||||
val (target, negative, overflow) = decodeCompact(bits)
|
||||
if (target == BigInteger.ZERO || negative || overflow) BigInt(0) else chainWork(target)
|
||||
}
|
||||
|
||||
def chainWork(header: BlockHeader): BigInt = chainWork(header.bits)
|
||||
|
||||
/**
|
||||
* Optimize blockchain
|
||||
*
|
||||
* @param blockchain
|
||||
* @param acc internal accumulator
|
||||
* @return a (blockchain, indexes) tuple where headers that are old enough have been removed and new checkpoints added,
|
||||
* and indexes is the list of header indexes that have been optimized out and must be persisted
|
||||
*/
|
||||
@tailrec
|
||||
def optimize(blockchain: Blockchain, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]) : (Blockchain, Vector[BlockIndex]) = {
|
||||
if (blockchain.bestchain.size >= RETARGETING_PERIOD + MAX_REORG) {
|
||||
val saveme = blockchain.bestchain.take(RETARGETING_PERIOD)
|
||||
val headersMap1 = blockchain.headersMap -- saveme.map(_.hash)
|
||||
val bestchain1 = blockchain.bestchain.drop(RETARGETING_PERIOD)
|
||||
val checkpoints1 = blockchain.checkpoints :+ CheckPoint(saveme.last.hash, bestchain1.head.header.bits)
|
||||
optimize(blockchain.copy(headersMap = headersMap1, bestchain = bestchain1, checkpoints = checkpoints1), acc ++ saveme)
|
||||
} else {
|
||||
(blockchain, acc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difficulty target at a given height.
|
||||
*
|
||||
* @param blockchain blockchain
|
||||
* @param height height for which we want the difficulty target
|
||||
* @param headerDb header database
|
||||
* @return the difficulty target for this height
|
||||
*/
|
||||
def getDifficulty(blockchain: Blockchain, height: Int, headerDb: HeaderDb): Option[Long] = {
|
||||
blockchain.chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash | Block.RegtestGenesisBlock.hash =>
|
||||
(height % RETARGETING_PERIOD) match {
|
||||
case 0 =>
|
||||
for {
|
||||
parent <- blockchain.getHeader(height - 1) orElse headerDb.getHeader(height - 1)
|
||||
previous <- blockchain.getHeader(height - 2016) orElse headerDb.getHeader(height - 2016)
|
||||
target = BlockHeader.calculateNextWorkRequired(parent, previous.time)
|
||||
} yield target
|
||||
case _ => blockchain.getHeader(height - 1) orElse headerDb.getHeader(height - 1) map (_.bits)
|
||||
}
|
||||
case _ => None // no difficulty check on testnet
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,79 +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.blockchain.electrum
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, encodeCompact}
|
||||
import fr.acinq.eclair.blockchain.electrum.db.HeaderDb
|
||||
import org.json4s.JsonAST.{JArray, JInt, JString}
|
||||
import org.json4s.jackson.JsonMethods
|
||||
|
||||
/**
|
||||
*
|
||||
* @param hash block hash
|
||||
* @param target difficulty target for the next block
|
||||
*/
|
||||
case class CheckPoint(hash: ByteVector32, nextBits: Long)
|
||||
|
||||
object CheckPoint {
|
||||
|
||||
import Blockchain.RETARGETING_PERIOD
|
||||
|
||||
/**
|
||||
* Load checkpoints.
|
||||
* There is one checkpoint every 2016 blocks (which is the difficulty adjustment period). They are used to check that
|
||||
* we're on the right chain and to validate proof-of-work by checking the difficulty target
|
||||
* @return an ordered list of checkpoints, with one checkpoint every 2016 blocks
|
||||
*/
|
||||
def load(chainHash: ByteVector32): Vector[CheckPoint] = chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash => load(classOf[CheckPoint].getResourceAsStream("/electrum/checkpoints_mainnet.json"))
|
||||
case Block.TestnetGenesisBlock.hash => load(classOf[CheckPoint].getResourceAsStream("/electrum/checkpoints_testnet.json"))
|
||||
case Block.RegtestGenesisBlock.hash => Vector.empty[CheckPoint] // no checkpoints on regtest
|
||||
}
|
||||
|
||||
def load(stream: InputStream): Vector[CheckPoint] = {
|
||||
val JArray(values) = JsonMethods.parse(stream)
|
||||
val checkpoints = values.collect {
|
||||
case JArray(JString(a) :: JInt(b) :: Nil) => CheckPoint(ByteVector32.fromValidHex(a).reverse, encodeCompact(b.bigInteger))
|
||||
}
|
||||
checkpoints.toVector
|
||||
}
|
||||
|
||||
/**
|
||||
* load checkpoints from our resources and header database
|
||||
*
|
||||
* @param chainHash chaim hash
|
||||
* @param headerDb header db
|
||||
* @return a series of checkpoints
|
||||
*/
|
||||
def load(chainHash: ByteVector32, headerDb: HeaderDb): Vector[CheckPoint] = {
|
||||
val checkpoints = CheckPoint.load(chainHash)
|
||||
val checkpoints1 = headerDb.getTip match {
|
||||
case Some((height, header)) =>
|
||||
val newcheckpoints = for {h <- checkpoints.size * RETARGETING_PERIOD - 1 + RETARGETING_PERIOD to height - RETARGETING_PERIOD by RETARGETING_PERIOD} yield {
|
||||
// we * should * have these headers in our db
|
||||
val cpheader = headerDb.getHeader(h).get
|
||||
val nextDiff = headerDb.getHeader(h + 1).get.bits
|
||||
CheckPoint(cpheader.hash, nextDiff)
|
||||
}
|
||||
checkpoints ++ newcheckpoints
|
||||
case None => checkpoints
|
||||
}
|
||||
checkpoints1
|
||||
}
|
||||
}
|
||||
@ -1,326 +1,213 @@
|
||||
/*
|
||||
* 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.blockchain.electrum
|
||||
|
||||
import java.net.{InetSocketAddress, SocketAddress}
|
||||
import java.util
|
||||
import java.io.InputStream
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated}
|
||||
import akka.io.{IO, Tcp}
|
||||
import akka.util.ByteString
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockCount
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCRequest, JsonRPCResponse}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.PooledByteBufAllocator
|
||||
import io.netty.channel._
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.handler.codec.string.{LineEncoder, StringDecoder}
|
||||
import io.netty.handler.codec.{LineBasedFrameDecoder, MessageToMessageDecoder, MessageToMessageEncoder}
|
||||
import io.netty.handler.ssl.SslContextBuilder
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory
|
||||
import io.netty.util.CharsetUtil
|
||||
import org.json4s.JsonAST._
|
||||
import org.json4s.jackson.JsonMethods
|
||||
import org.json4s.{DefaultFormats, JInt, JLong, JString}
|
||||
import scodec.bits.ByteVector
|
||||
import org.spongycastle.util.encoders.Hex
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* For later optimizations, see http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html
|
||||
*
|
||||
*/
|
||||
class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec: ExecutionContext) extends Actor with Stash with ActorLogging {
|
||||
class ElectrumClient(serverAddresses: Seq[InetSocketAddress]) extends Actor with Stash with ActorLogging {
|
||||
|
||||
import ElectrumClient._
|
||||
import context.system
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
val b = new Bootstrap
|
||||
b.group(workerGroup)
|
||||
b.channel(classOf[NioSocketChannel])
|
||||
b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true)
|
||||
b.option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true)
|
||||
b.option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
|
||||
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
|
||||
b.handler(new ChannelInitializer[SocketChannel]() {
|
||||
override def initChannel(ch: SocketChannel): Unit = {
|
||||
ssl match {
|
||||
case SSL.OFF => ()
|
||||
case SSL.STRICT =>
|
||||
val sslCtx = SslContextBuilder.forClient.build
|
||||
ch.pipeline.addLast(sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort))
|
||||
case SSL.LOOSE =>
|
||||
// INSECURE VERSION THAT DOESN'T CHECK CERTIFICATE
|
||||
val sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()
|
||||
ch.pipeline.addLast(sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort))
|
||||
}
|
||||
// inbound handlers
|
||||
ch.pipeline.addLast(new LineBasedFrameDecoder(Int.MaxValue, true, true)) // JSON messages are separated by a new line
|
||||
ch.pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8))
|
||||
ch.pipeline.addLast(new ElectrumResponseDecoder)
|
||||
ch.pipeline.addLast(new ActorHandler(self))
|
||||
// outbound handlers
|
||||
ch.pipeline.addLast(new LineEncoder)
|
||||
ch.pipeline.addLast(new JsonRPCRequestEncoder)
|
||||
// error handler
|
||||
ch.pipeline.addLast(new ExceptionHandler)
|
||||
}
|
||||
})
|
||||
|
||||
// Start the client.
|
||||
log.info("connecting to server={}", serverAddress)
|
||||
|
||||
val channelOpenFuture = b.connect(serverAddress.getHostName, serverAddress.getPort)
|
||||
|
||||
def errorHandler(t: Throwable) = {
|
||||
log.info("server={} connection error (reason={})", serverAddress, t.getMessage)
|
||||
self ! Close
|
||||
}
|
||||
|
||||
channelOpenFuture.addListeners(new ChannelFutureListener {
|
||||
override def operationComplete(future: ChannelFuture): Unit = {
|
||||
if (!future.isSuccess) {
|
||||
errorHandler(future.cause())
|
||||
} else {
|
||||
future.channel().closeFuture().addListener(new ChannelFutureListener {
|
||||
override def operationComplete(future: ChannelFuture): Unit = {
|
||||
if (!future.isSuccess) {
|
||||
errorHandler(future.cause())
|
||||
} else {
|
||||
log.info("server={} channel closed: {}", serverAddress, future.channel())
|
||||
self ! Close
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This error handler catches all exceptions and kill the actor
|
||||
* See https://stackoverflow.com/questions/30994095/how-to-catch-all-exception-in-netty
|
||||
*/
|
||||
class ExceptionHandler extends ChannelDuplexHandler {
|
||||
override def connect(ctx: ChannelHandlerContext, remoteAddress: SocketAddress, localAddress: SocketAddress, promise: ChannelPromise): Unit = {
|
||||
ctx.connect(remoteAddress, localAddress, promise.addListener(new ChannelFutureListener() {
|
||||
override def operationComplete(future: ChannelFuture): Unit = {
|
||||
if (!future.isSuccess) {
|
||||
errorHandler(future.cause())
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def write(ctx: ChannelHandlerContext, msg: scala.Any, promise: ChannelPromise): Unit = {
|
||||
ctx.write(msg, promise.addListener(new ChannelFutureListener() {
|
||||
override def operationComplete(future: ChannelFuture): Unit = {
|
||||
if (!future.isSuccess) {
|
||||
errorHandler(future.cause())
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = {
|
||||
errorHandler(cause)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A decoder ByteBuf -> Either[Response, JsonRPCResponse]
|
||||
*/
|
||||
class ElectrumResponseDecoder extends MessageToMessageDecoder[String] {
|
||||
override def decode(ctx: ChannelHandlerContext, msg: String, out: util.List[AnyRef]): Unit = {
|
||||
val s = msg.asInstanceOf[String]
|
||||
val r = parseResponse(s)
|
||||
out.add(r)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An encoder JsonRPCRequest -> ByteBuf
|
||||
*/
|
||||
class JsonRPCRequestEncoder extends MessageToMessageEncoder[JsonRPCRequest] {
|
||||
override def encode(ctx: ChannelHandlerContext, request: JsonRPCRequest, out: util.List[AnyRef]): Unit = {
|
||||
import org.json4s.JsonDSL._
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.JsonMethods._
|
||||
|
||||
log.debug("sending {} to {}", request, serverAddress)
|
||||
val json = ("method" -> request.method) ~ ("params" -> request.params.map {
|
||||
case s: String => new JString(s)
|
||||
case b: ByteVector32 => new JString(b.toHex)
|
||||
case t: Int => new JInt(t)
|
||||
case t: Long => new JLong(t)
|
||||
case t: Double => new JDouble(t)
|
||||
}) ~ ("id" -> request.id) ~ ("jsonrpc" -> request.jsonrpc)
|
||||
val serialized = compact(render(json))
|
||||
out.add(serialized)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Forwards incoming messages to the underlying actor
|
||||
*
|
||||
* @param actor
|
||||
*/
|
||||
class ActorHandler(actor: ActorRef) extends ChannelInboundHandlerAdapter {
|
||||
|
||||
override def channelActive(ctx: ChannelHandlerContext): Unit = {
|
||||
actor ! ctx
|
||||
}
|
||||
|
||||
override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = {
|
||||
actor ! msg
|
||||
}
|
||||
}
|
||||
|
||||
var addressSubscriptions = Map.empty[String, Set[ActorRef]]
|
||||
var scriptHashSubscriptions = Map.empty[ByteVector32, Set[ActorRef]]
|
||||
val headerSubscriptions = collection.mutable.HashSet.empty[ActorRef]
|
||||
val version = ServerVersion(CLIENT_NAME, PROTOCOL_VERSION)
|
||||
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
|
||||
|
||||
var reqId = 0
|
||||
val newline = "\n"
|
||||
val connectionFailures = collection.mutable.HashMap.empty[InetSocketAddress, Long]
|
||||
|
||||
val version = ServerVersion("2.1.7", "1.1")
|
||||
// we need to regularly send a ping in order not to get disconnected
|
||||
val pingTrigger = context.system.scheduler.schedule(30 seconds, 30 seconds, self, Ping)
|
||||
context.system.scheduler.schedule(30 seconds, 30 seconds, self, version)
|
||||
|
||||
override def unhandled(message: Any): Unit = {
|
||||
message match {
|
||||
case _: Tcp.ConnectionClosed =>
|
||||
val nextAddress = nextPeer()
|
||||
log.warning(s"connection failed, trying $nextAddress")
|
||||
self ! Tcp.Connect(nextAddress)
|
||||
statusListeners.map(_ ! ElectrumDisconnected)
|
||||
context.system.eventStream.publish(ElectrumDisconnected)
|
||||
context become disconnected
|
||||
|
||||
case Terminated(deadActor) =>
|
||||
addressSubscriptions = addressSubscriptions.mapValues(subscribers => subscribers - deadActor)
|
||||
scriptHashSubscriptions = scriptHashSubscriptions.mapValues(subscribers => subscribers - deadActor)
|
||||
val removeMe = addressSubscriptions collect {
|
||||
case (address, actor) if actor == deadActor => address
|
||||
}
|
||||
addressSubscriptions --= removeMe
|
||||
|
||||
val removeMe1 = scriptHashSubscriptions collect {
|
||||
case (scriptHash, actor) if actor == deadActor => scriptHash
|
||||
}
|
||||
scriptHashSubscriptions --= removeMe1
|
||||
statusListeners -= deadActor
|
||||
headerSubscriptions -= deadActor
|
||||
|
||||
case RemoveStatusListener(actor) => statusListeners -= actor
|
||||
case _: ServerVersion => () // we only handle this when connected
|
||||
|
||||
case PingResponse => ()
|
||||
case _: ServerVersionResponse => () // we just ignore these messages, they are used as pings
|
||||
|
||||
case Close =>
|
||||
statusListeners.map(_ ! ElectrumDisconnected)
|
||||
context.stop(self)
|
||||
|
||||
case _ => log.warning("server={} unhandled message {}", serverAddress, message)
|
||||
case _ => log.warning(s"unhandled $message")
|
||||
}
|
||||
}
|
||||
|
||||
override def postStop(): Unit = {
|
||||
pingTrigger.cancel()
|
||||
super.postStop()
|
||||
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
|
||||
|
||||
def send(connection: ActorRef, request: JsonRPCRequest): Unit = {
|
||||
import org.json4s.JsonDSL._
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.JsonMethods._
|
||||
|
||||
log.debug(s"sending $request")
|
||||
val json = ("method" -> request.method) ~ ("params" -> request.params.map {
|
||||
case s: String => new JString(s)
|
||||
case b: BinaryData => new JString(b.toString())
|
||||
case t: Int => new JInt(t)
|
||||
case t: Long => new JLong(t)
|
||||
case t: Double => new JDouble(t)
|
||||
}) ~ ("id" -> request.id) ~ ("jsonrpc" -> request.jsonrpc)
|
||||
val serialized = compact(render(json))
|
||||
val bytes = (serialized + newline).getBytes
|
||||
connection ! Tcp.Write(ByteString.fromArray(bytes))
|
||||
}
|
||||
|
||||
/**
|
||||
* send an electrum request to the server
|
||||
*
|
||||
* @param ctx connection to the electrumx server
|
||||
* @param request electrum request
|
||||
* @return the request id used to send the request
|
||||
*/
|
||||
def send(ctx: ChannelHandlerContext, request: Request): String = {
|
||||
val electrumRequestId = "" + reqId
|
||||
if (ctx.channel().isWritable) {
|
||||
ctx.channel().writeAndFlush(makeRequest(request, electrumRequestId))
|
||||
} else {
|
||||
errorHandler(new RuntimeException(s"channel not writable"))
|
||||
}
|
||||
reqId = reqId + 1
|
||||
electrumRequestId
|
||||
private def nextPeer() = {
|
||||
val nextPos = Random.nextInt(serverAddresses.size)
|
||||
serverAddresses(nextPos)
|
||||
}
|
||||
|
||||
private def updateBlockCount(blockCount: Long) = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
log.debug(s"current blockchain height=$blockCount")
|
||||
system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
}
|
||||
|
||||
val addressSubscriptions = collection.mutable.HashMap.empty[String, Set[ActorRef]]
|
||||
val scriptHashSubscriptions = collection.mutable.HashMap.empty[BinaryData, Set[ActorRef]]
|
||||
val headerSubscriptions = collection.mutable.HashSet.empty[ActorRef]
|
||||
|
||||
context.system.eventStream.publish(ElectrumDisconnected)
|
||||
self ! Tcp.Connect(serverAddresses.head)
|
||||
|
||||
var reqId = 0L
|
||||
|
||||
def receive = disconnected
|
||||
|
||||
def disconnected: Receive = {
|
||||
case ctx: ChannelHandlerContext =>
|
||||
log.info("connected to server={}", serverAddress)
|
||||
send(ctx, version)
|
||||
context become waitingForVersion(ctx)
|
||||
case c: Tcp.Connect =>
|
||||
log.info(s"connecting to $c")
|
||||
IO(Tcp) ! c
|
||||
|
||||
case Tcp.Connected(remote, _) =>
|
||||
log.info(s"connected to $remote")
|
||||
connectionFailures.clear()
|
||||
val connection = sender()
|
||||
connection ! Tcp.Register(self)
|
||||
val request = version
|
||||
send(connection, makeRequest(request, "" + reqId))
|
||||
reqId = reqId + 1
|
||||
context become waitingForVersion(connection, remote)
|
||||
|
||||
case AddStatusListener(actor) => statusListeners += actor
|
||||
|
||||
case Tcp.CommandFailed(Tcp.Connect(remoteAddress, _, _, _, _)) =>
|
||||
val nextAddress = nextPeer()
|
||||
log.warning(s"connection to $remoteAddress failed, trying $nextAddress")
|
||||
connectionFailures.put(remoteAddress, connectionFailures.getOrElse(remoteAddress, 0L) + 1L)
|
||||
val count = connectionFailures.getOrElse(nextAddress, 0L)
|
||||
val delay = Math.min(Math.pow(2.0, count), 60.0) seconds;
|
||||
context.system.scheduler.scheduleOnce(delay, self, Tcp.Connect(nextAddress))
|
||||
}
|
||||
|
||||
def waitingForVersion(connection: ActorRef, remote: InetSocketAddress): Receive = {
|
||||
case Tcp.Received(data) =>
|
||||
val response = parseResponse(new String(data.toArray)).right.get
|
||||
val serverVersion = parseJsonResponse(version, response)
|
||||
log.debug(s"serverVersion=$serverVersion")
|
||||
val request = HeaderSubscription(self)
|
||||
send(connection, makeRequest(request, "" + reqId))
|
||||
headerSubscriptions += self
|
||||
log.debug("waiting for tip")
|
||||
reqId = reqId + 1
|
||||
context become waitingForTip(connection, remote: InetSocketAddress)
|
||||
|
||||
case AddStatusListener(actor) => statusListeners += actor
|
||||
}
|
||||
|
||||
def waitingForVersion(ctx: ChannelHandlerContext): Receive = {
|
||||
case Right(json: JsonRPCResponse) =>
|
||||
(parseJsonResponse(version, json): @unchecked) match {
|
||||
case ServerVersionResponse(clientName, protocolVersion) =>
|
||||
log.info("server={} clientName={} protocolVersion={}", serverAddress, clientName, protocolVersion)
|
||||
send(ctx, HeaderSubscription(self))
|
||||
headerSubscriptions += self
|
||||
log.debug("waiting for tip from server={}", serverAddress)
|
||||
context become waitingForTip(ctx)
|
||||
case ServerError(request, error) =>
|
||||
log.error("server={} sent error={} while processing request={}, disconnecting", serverAddress, error, request)
|
||||
self ! Close
|
||||
}
|
||||
def waitingForTip(connection: ActorRef, remote: InetSocketAddress): Receive = {
|
||||
case Tcp.Received(data) =>
|
||||
val response = parseResponse(new String(data.toArray)).right.get
|
||||
val header = parseHeader(response.result)
|
||||
log.debug(s"connected, tip = ${header.block_hash} $header")
|
||||
updateBlockCount(header.block_height)
|
||||
statusListeners.map(_ ! ElectrumReady)
|
||||
context.system.eventStream.publish(ElectrumConnected)
|
||||
context become connected(connection, remote, header, "", Map.empty)
|
||||
|
||||
case AddStatusListener(actor) => statusListeners += actor
|
||||
}
|
||||
|
||||
def waitingForTip(ctx: ChannelHandlerContext): Receive = {
|
||||
case Right(json: JsonRPCResponse) =>
|
||||
val (height, header) = parseBlockHeader(json.result)
|
||||
log.debug("connected to server={}, tip={} height={}", serverAddress, header.hash, height)
|
||||
statusListeners.map(_ ! ElectrumReady(height, header, serverAddress))
|
||||
context become connected(ctx, height, header, Map())
|
||||
|
||||
case AddStatusListener(actor) => statusListeners += actor
|
||||
}
|
||||
|
||||
def connected(ctx: ChannelHandlerContext, height: Int, tip: BlockHeader, requests: Map[String, (Request, ActorRef)]): Receive = {
|
||||
def connected(connection: ActorRef, remoteAddress: InetSocketAddress, tip: Header, buffer: String, requests: Map[String, (Request, ActorRef)]): Receive = {
|
||||
case AddStatusListener(actor) =>
|
||||
statusListeners += actor
|
||||
actor ! ElectrumReady(height, tip, serverAddress)
|
||||
actor ! ElectrumReady
|
||||
|
||||
case HeaderSubscription(actor) =>
|
||||
headerSubscriptions += actor
|
||||
actor ! HeaderSubscriptionResponse(height, tip)
|
||||
actor ! HeaderSubscriptionResponse(tip)
|
||||
context watch actor
|
||||
|
||||
case request: Request =>
|
||||
val curReqId = send(ctx, request)
|
||||
val curReqId = "" + reqId
|
||||
send(connection, makeRequest(request, curReqId))
|
||||
request match {
|
||||
case AddressSubscription(address, actor) =>
|
||||
addressSubscriptions = addressSubscriptions.updated(address, addressSubscriptions.getOrElse(address, Set()) + actor)
|
||||
addressSubscriptions.update(address, addressSubscriptions.getOrElse(address, Set()) + actor)
|
||||
context watch actor
|
||||
case ScriptHashSubscription(scriptHash, actor) =>
|
||||
scriptHashSubscriptions = scriptHashSubscriptions.updated(scriptHash, scriptHashSubscriptions.getOrElse(scriptHash, Set()) + actor)
|
||||
scriptHashSubscriptions.update(scriptHash, scriptHashSubscriptions.getOrElse(scriptHash, Set()) + actor)
|
||||
context watch actor
|
||||
case _ => ()
|
||||
}
|
||||
context become connected(ctx, height, tip, requests + (curReqId -> (request, sender())))
|
||||
reqId = reqId + 1
|
||||
context become connected(connection, remoteAddress, tip, buffer, requests + (curReqId -> (request, sender())))
|
||||
|
||||
case Tcp.Received(data) =>
|
||||
val buffer1 = buffer + new String(data.toArray)
|
||||
val (jsons, buffer2) = buffer1.split(newline) match {
|
||||
case chunks if buffer1.endsWith(newline) => (chunks, "")
|
||||
case chunks => (chunks.dropRight(1), chunks.last)
|
||||
}
|
||||
jsons.map(parseResponse(_)).map(self ! _)
|
||||
context become connected(connection, remoteAddress, tip, buffer2, requests)
|
||||
|
||||
case Right(json: JsonRPCResponse) =>
|
||||
requests.get(json.id) match {
|
||||
case Some((request, requestor)) =>
|
||||
val response = parseJsonResponse(request, json)
|
||||
log.debug("server={} sent response for reqId={} request={} response={}", serverAddress, json.id, request, response)
|
||||
log.debug(s"got response for reqId=${json.id} request=$request response=$response")
|
||||
requestor ! response
|
||||
case None =>
|
||||
log.warning("server={} could not find requestor for reqId=${} response={}", serverAddress, json.id, json)
|
||||
log.warning(s"could not find requestor for reqId=${json.id} response=$json")
|
||||
}
|
||||
context become connected(ctx, height, tip, requests - json.id)
|
||||
context become connected(connection, remoteAddress, tip, buffer, requests - json.id)
|
||||
|
||||
case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.map(_ ! response)
|
||||
|
||||
@ -328,18 +215,20 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
|
||||
|
||||
case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).map(listeners => listeners.map(_ ! response))
|
||||
|
||||
case HeaderSubscriptionResponse(height, newtip) =>
|
||||
log.info("server={} new tip={}", serverAddress, newtip)
|
||||
context become connected(ctx, height, newtip, requests)
|
||||
case HeaderSubscriptionResponse(newtip) =>
|
||||
log.info(s"new tip $newtip")
|
||||
updateBlockCount(newtip.block_height)
|
||||
context become connected(connection, remoteAddress, newtip, buffer, requests)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ElectrumClient {
|
||||
val CLIENT_NAME = "3.3.4" // client name that we will include in our "version" message
|
||||
val PROTOCOL_VERSION = "1.4" // version of the protocol that we require
|
||||
|
||||
// this is expensive and shared with all clients
|
||||
val workerGroup = new NioEventLoopGroup()
|
||||
def apply(addresses: java.util.List[InetSocketAddress]): ElectrumClient = {
|
||||
import collection.JavaConversions._
|
||||
new ElectrumClient(addresses)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to converts a publicKeyScript to electrum's scripthash
|
||||
@ -347,92 +236,73 @@ object ElectrumClient {
|
||||
* @param publicKeyScript public key script
|
||||
* @return the hash of the public key script, as used by ElectrumX's hash-based methods
|
||||
*/
|
||||
def computeScriptHash(publicKeyScript: ByteVector): ByteVector32 = Crypto.sha256(publicKeyScript).reverse
|
||||
def computeScriptHash(publicKeyScript: BinaryData): BinaryData = Crypto.sha256(publicKeyScript).reverse
|
||||
|
||||
// @formatter:off
|
||||
case class AddStatusListener(actor: ActorRef)
|
||||
case class RemoveStatusListener(actor: ActorRef)
|
||||
|
||||
sealed trait Request
|
||||
sealed trait Response
|
||||
|
||||
case class ServerVersion(clientName: String, protocolVersion: String) extends Request
|
||||
case class ServerVersionResponse(clientName: String, protocolVersion: String) extends Response
|
||||
|
||||
case object Ping extends Request
|
||||
case object PingResponse extends Response
|
||||
|
||||
case class GetAddressHistory(address: String) extends Request
|
||||
case class TransactionHistoryItem(height: Int, tx_hash: ByteVector32)
|
||||
case class TransactionHistoryItem(height: Long, tx_hash: BinaryData)
|
||||
case class GetAddressHistoryResponse(address: String, history: Seq[TransactionHistoryItem]) extends Response
|
||||
|
||||
case class GetScriptHashHistory(scriptHash: ByteVector32) extends Request
|
||||
case class GetScriptHashHistoryResponse(scriptHash: ByteVector32, history: List[TransactionHistoryItem]) extends Response
|
||||
case class GetScriptHashHistory(scriptHash: BinaryData) extends Request
|
||||
case class GetScriptHashHistoryResponse(scriptHash: BinaryData, history: Seq[TransactionHistoryItem]) extends Response
|
||||
|
||||
case class AddressListUnspent(address: String) extends Request
|
||||
case class UnspentItem(tx_hash: ByteVector32, tx_pos: Int, value: Long, height: Long) {
|
||||
case class UnspentItem(tx_hash: BinaryData, tx_pos: Int, value: Long, height: Long) {
|
||||
lazy val outPoint = OutPoint(tx_hash.reverse, tx_pos)
|
||||
}
|
||||
case class AddressListUnspentResponse(address: String, unspents: Seq[UnspentItem]) extends Response
|
||||
|
||||
case class ScriptHashListUnspent(scriptHash: ByteVector32) extends Request
|
||||
case class ScriptHashListUnspentResponse(scriptHash: ByteVector32, unspents: Seq[UnspentItem]) extends Response
|
||||
case class ScriptHashListUnspent(scriptHash: BinaryData) extends Request
|
||||
case class ScriptHashListUnspentResponse(scriptHash: BinaryData, unspents: Seq[UnspentItem]) extends Response
|
||||
|
||||
case class BroadcastTransaction(tx: Transaction) extends Request
|
||||
case class BroadcastTransactionResponse(tx: Transaction, error: Option[Error]) extends Response
|
||||
|
||||
case class GetTransaction(txid: ByteVector32) extends Request
|
||||
case class GetTransaction(txid: BinaryData) extends Request
|
||||
case class GetTransactionResponse(tx: Transaction) extends Response
|
||||
|
||||
case class GetHeader(height: Int) extends Request
|
||||
case class GetHeaderResponse(height: Int, header: BlockHeader) extends Response
|
||||
object GetHeaderResponse {
|
||||
def apply(t: (Int, BlockHeader)) = new GetHeaderResponse(t._1, t._2)
|
||||
}
|
||||
|
||||
case class GetHeaders(start_height: Int, count: Int, cp_height: Int = 0) extends Request
|
||||
case class GetHeadersResponse(start_height: Int, headers: Seq[BlockHeader], max: Int) extends Response
|
||||
|
||||
case class GetMerkle(txid: ByteVector32, height: Int) extends Request
|
||||
case class GetMerkleResponse(txid: ByteVector32, merkle: List[ByteVector32], block_height: Int, pos: Int) extends Response {
|
||||
lazy val root: ByteVector32 = {
|
||||
case class GetMerkle(txid: BinaryData, height: Long) extends Request
|
||||
case class GetMerkleResponse(txid: BinaryData, merkle: Seq[BinaryData], block_height: Long, pos: Int) extends Response {
|
||||
lazy val root: BinaryData = {
|
||||
@tailrec
|
||||
def loop(pos: Int, hashes: Seq[ByteVector32]): ByteVector32 = {
|
||||
if (hashes.length == 1) hashes(0)
|
||||
def loop(pos: Int, hashes: Seq[BinaryData]): BinaryData = {
|
||||
if (hashes.length == 1) hashes(0).reverse
|
||||
else {
|
||||
val h = if (pos % 2 == 1) Crypto.hash256(hashes(1) ++ hashes(0)) else Crypto.hash256(hashes(0) ++ hashes(1))
|
||||
loop(pos / 2, h +: hashes.drop(2))
|
||||
}
|
||||
}
|
||||
loop(pos, txid.reverse +: merkle.map(b => b.reverse))
|
||||
loop(pos, BinaryData(txid.reverse) +: merkle.map(b => BinaryData(b.reverse)))
|
||||
}
|
||||
}
|
||||
|
||||
case class AddressSubscription(address: String, actor: ActorRef) extends Request
|
||||
case class AddressSubscriptionResponse(address: String, status: String) extends Response
|
||||
|
||||
case class ScriptHashSubscription(scriptHash: ByteVector32, actor: ActorRef) extends Request
|
||||
case class ScriptHashSubscriptionResponse(scriptHash: ByteVector32, status: String) extends Response
|
||||
case class ScriptHashSubscription(scriptHash: BinaryData, actor: ActorRef) extends Request
|
||||
case class ScriptHashSubscriptionResponse(scriptHash: BinaryData, status: String) extends Response
|
||||
|
||||
case class HeaderSubscription(actor: ActorRef) extends Request
|
||||
case class HeaderSubscriptionResponse(height: Int, header: BlockHeader) extends Response
|
||||
object HeaderSubscriptionResponse {
|
||||
def apply(t: (Int, BlockHeader)) = new HeaderSubscriptionResponse(t._1, t._2)
|
||||
}
|
||||
case class HeaderSubscriptionResponse(header: Header) extends Response
|
||||
|
||||
case class Header(block_height: Long, version: Long, prev_block_hash: ByteVector32, merkle_root: ByteVector32, timestamp: Long, bits: Long, nonce: Long) {
|
||||
def blockHeader = BlockHeader(version, prev_block_hash.reverse, merkle_root.reverse, timestamp, bits, nonce)
|
||||
|
||||
lazy val block_hash: ByteVector32 = blockHeader.hash
|
||||
lazy val block_id: ByteVector32 = block_hash.reverse
|
||||
case class Header(block_height: Long, version: Long, prev_block_hash: BinaryData, merkle_root: BinaryData, timestamp: Long, bits: Long, nonce: Long) {
|
||||
lazy val block_hash: BinaryData = {
|
||||
val blockHeader = BlockHeader(version, prev_block_hash.reverse, merkle_root.reverse, timestamp, bits, nonce)
|
||||
blockHeader.hash.reverse
|
||||
}
|
||||
}
|
||||
|
||||
object Header {
|
||||
def makeHeader(height: Long, header: BlockHeader) = ElectrumClient.Header(height, header.version, header.hashPreviousBlock.reverse, header.hashMerkleRoot.reverse, header.time, header.bits, header.nonce)
|
||||
def makeHeader(height: Long, header: BlockHeader) = ElectrumClient.Header(0, header.version, header.hashPreviousBlock, header.hashMerkleRoot, header.time, header.bits, header.nonce)
|
||||
|
||||
val RegtestGenesisHeader = makeHeader(0, Block.RegtestGenesisBlock.header)
|
||||
val TestnetGenesisHeader = makeHeader(0, Block.TestnetGenesisBlock.header)
|
||||
val LivenetGenesisHeader = makeHeader(0, Block.LivenetGenesisBlock.header)
|
||||
}
|
||||
|
||||
case class TransactionHistory(history: Seq[TransactionHistoryItem]) extends Response
|
||||
@ -440,24 +310,13 @@ object ElectrumClient {
|
||||
case class AddressStatus(address: String, status: String) extends Response
|
||||
|
||||
case class ServerError(request: Request, error: Error) extends Response
|
||||
case class AddStatusListener(actor: ActorRef) extends Response
|
||||
|
||||
sealed trait ElectrumEvent
|
||||
|
||||
case class ElectrumReady(height: Int, tip: BlockHeader, serverAddress: InetSocketAddress) extends ElectrumEvent
|
||||
object ElectrumReady {
|
||||
def apply(t: (Int, BlockHeader), serverAddress: InetSocketAddress) = new ElectrumReady(t._1 , t._2, serverAddress)
|
||||
}
|
||||
case object ElectrumConnected extends ElectrumEvent
|
||||
case object ElectrumReady extends ElectrumEvent
|
||||
case object ElectrumDisconnected extends ElectrumEvent
|
||||
|
||||
sealed trait SSL
|
||||
object SSL {
|
||||
case object OFF extends SSL
|
||||
case object STRICT extends SSL
|
||||
case object LOOSE extends SSL
|
||||
}
|
||||
|
||||
case object Close
|
||||
|
||||
// @formatter:on
|
||||
|
||||
def parseResponse(input: String): Either[Response, JsonRPCResponse] = {
|
||||
@ -468,11 +327,11 @@ object ElectrumClient {
|
||||
// this is a jsonrpc request, i.e. a subscription response
|
||||
val JArray(params) = json \ "params"
|
||||
Left(((method, params): @unchecked) match {
|
||||
case ("blockchain.headers.subscribe", header :: Nil) => HeaderSubscriptionResponse(parseBlockHeader(header))
|
||||
case ("blockchain.headers.subscribe", header :: Nil) => HeaderSubscriptionResponse(parseHeader(header))
|
||||
case ("blockchain.address.subscribe", JString(address) :: JNull :: Nil) => AddressSubscriptionResponse(address, "")
|
||||
case ("blockchain.address.subscribe", JString(address) :: JString(status) :: Nil) => AddressSubscriptionResponse(address, status)
|
||||
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JNull :: Nil) => ScriptHashSubscriptionResponse(ByteVector32.fromValidHex(scriptHashHex), "")
|
||||
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JString(status) :: Nil) => ScriptHashSubscriptionResponse(ByteVector32.fromValidHex(scriptHashHex), status)
|
||||
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JNull :: Nil) => ScriptHashSubscriptionResponse(BinaryData(scriptHashHex), "")
|
||||
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JString(status) :: Nil) => ScriptHashSubscriptionResponse(BinaryData(scriptHashHex), status)
|
||||
})
|
||||
case _ => Right(parseJsonRpcResponse(json))
|
||||
}
|
||||
@ -515,26 +374,28 @@ object ElectrumClient {
|
||||
case JInt(value) => value.intValue()
|
||||
}
|
||||
|
||||
def parseBlockHeader(json: JValue): (Int, BlockHeader) = {
|
||||
val height = intField(json, "height")
|
||||
val JString(hex) = json \ "hex"
|
||||
(height, BlockHeader.read(hex))
|
||||
def parseHeader(json: JValue): Header = {
|
||||
val block_height = longField(json, "block_height")
|
||||
val version = longField(json, "version")
|
||||
val timestamp = longField(json, "timestamp")
|
||||
val bits = longField(json, "bits")
|
||||
val nonce = longField(json, "nonce")
|
||||
val JString(prev_block_hash) = json \ "prev_block_hash"
|
||||
val JString(merkle_root) = json \ "merkle_root"
|
||||
Header(block_height, version, prev_block_hash, merkle_root, timestamp, bits, nonce)
|
||||
}
|
||||
|
||||
def makeRequest(request: Request, reqId: String): JsonRPCRequest = request match {
|
||||
case ServerVersion(clientName, protocolVersion) => JsonRPCRequest(id = reqId, method = "server.version", params = clientName :: protocolVersion :: Nil)
|
||||
case Ping => JsonRPCRequest(id = reqId, method = "server.ping", params = Nil)
|
||||
case GetAddressHistory(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.get_history", params = address :: Nil)
|
||||
case GetScriptHashHistory(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.get_history", params = scripthash.toHex :: Nil)
|
||||
case GetScriptHashHistory(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.get_history", params = scripthash.toString() :: Nil)
|
||||
case AddressListUnspent(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.listunspent", params = address :: Nil)
|
||||
case ScriptHashListUnspent(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.listunspent", params = scripthash.toHex :: Nil)
|
||||
case ScriptHashListUnspent(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.listunspent", params = scripthash.toString() :: Nil)
|
||||
case AddressSubscription(address, _) => JsonRPCRequest(id = reqId, method = "blockchain.address.subscribe", params = address :: Nil)
|
||||
case ScriptHashSubscription(scriptHash, _) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.subscribe", params = scriptHash.toString() :: Nil)
|
||||
case BroadcastTransaction(tx) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.broadcast", params = Transaction.write(tx).toHex :: Nil)
|
||||
case GetTransaction(txid) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get", params = txid :: Nil)
|
||||
case BroadcastTransaction(tx) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.broadcast", params = Hex.toHexString(Transaction.write(tx)) :: Nil)
|
||||
case GetTransaction(txid: BinaryData) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get", params = txid :: Nil)
|
||||
case HeaderSubscription(_) => JsonRPCRequest(id = reqId, method = "blockchain.headers.subscribe", params = Nil)
|
||||
case GetHeader(height) => JsonRPCRequest(id = reqId, method = "blockchain.block.header", params = height :: Nil)
|
||||
case GetHeaders(start_height, count, cp_height) => JsonRPCRequest(id = reqId, method = "blockchain.block.headers", params = start_height :: count :: Nil)
|
||||
case GetMerkle(txid, height) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get_merkle", params = txid :: height :: Nil)
|
||||
}
|
||||
|
||||
@ -551,21 +412,20 @@ object ElectrumClient {
|
||||
val JString(clientName) = jitems(0)
|
||||
val JString(protocolVersion) = jitems(1)
|
||||
ServerVersionResponse(clientName, protocolVersion)
|
||||
case Ping => PingResponse
|
||||
case GetAddressHistory(address) =>
|
||||
val JArray(jitems) = json.result
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "tx_hash"
|
||||
val height = intField(jvalue, "height")
|
||||
TransactionHistoryItem(height, ByteVector32.fromValidHex(tx_hash))
|
||||
val height = longField(jvalue, "height")
|
||||
TransactionHistoryItem(height, tx_hash)
|
||||
})
|
||||
GetAddressHistoryResponse(address, items)
|
||||
case GetScriptHashHistory(scripthash) =>
|
||||
val JArray(jitems) = json.result
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "tx_hash"
|
||||
val height = intField(jvalue, "height")
|
||||
TransactionHistoryItem(height, ByteVector32.fromValidHex(tx_hash))
|
||||
val height = longField(jvalue, "height")
|
||||
TransactionHistoryItem(height, tx_hash)
|
||||
})
|
||||
GetScriptHashHistoryResponse(scripthash, items)
|
||||
case AddressListUnspent(address) =>
|
||||
@ -573,9 +433,9 @@ object ElectrumClient {
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "tx_hash"
|
||||
val tx_pos = intField(jvalue, "tx_pos")
|
||||
val height = intField(jvalue, "height")
|
||||
val height = longField(jvalue, "height")
|
||||
val value = longField(jvalue, "value")
|
||||
UnspentItem(ByteVector32.fromValidHex(tx_hash), tx_pos, value, height)
|
||||
UnspentItem(tx_hash, tx_pos, value, height)
|
||||
})
|
||||
AddressListUnspentResponse(address, items)
|
||||
case ScriptHashListUnspent(scripthash) =>
|
||||
@ -585,7 +445,7 @@ object ElectrumClient {
|
||||
val tx_pos = intField(jvalue, "tx_pos")
|
||||
val height = longField(jvalue, "height")
|
||||
val value = longField(jvalue, "value")
|
||||
UnspentItem(ByteVector32.fromValidHex(tx_hash), tx_pos, value, height)
|
||||
UnspentItem(tx_hash, tx_pos, value, height)
|
||||
})
|
||||
ScriptHashListUnspentResponse(scripthash, items)
|
||||
case GetTransaction(_) =>
|
||||
@ -600,32 +460,29 @@ object ElectrumClient {
|
||||
case _ => ScriptHashSubscriptionResponse(scriptHash, "")
|
||||
}
|
||||
case BroadcastTransaction(tx) =>
|
||||
val JString(message) = json.result
|
||||
// if we got here, it means that the server's response does not contain an error and message should be our
|
||||
// transaction id. However, it seems that at least on testnet some servers still use an older version of the
|
||||
// Electrum protocol and return an error message in the result field
|
||||
Try(ByteVector32.fromValidHex(message)) match {
|
||||
case Success(txid) if txid == tx.txid => BroadcastTransactionResponse(tx, None)
|
||||
case Success(txid) => BroadcastTransactionResponse(tx, Some(Error(1, s"response txid $txid does not match request txid ${tx.txid}")))
|
||||
case Failure(_) => BroadcastTransactionResponse(tx, Some(Error(1, message)))
|
||||
}
|
||||
case GetHeader(height) =>
|
||||
val JString(hex) = json.result
|
||||
GetHeaderResponse(height, BlockHeader.read(hex))
|
||||
case GetHeaders(start_height, count, cp_height) =>
|
||||
val count = intField(json.result, "count")
|
||||
val max = intField(json.result, "max")
|
||||
val JString(hex) = json.result \ "hex"
|
||||
val bin = ByteVector.fromValidHex(hex).toArray
|
||||
val blockHeaders = bin.grouped(80).map(BlockHeader.read).toList
|
||||
GetHeadersResponse(start_height, blockHeaders, max)
|
||||
val JString(txid) = json.result
|
||||
require(BinaryData(txid) == tx.txid)
|
||||
BroadcastTransactionResponse(tx, None)
|
||||
case GetMerkle(txid, height) =>
|
||||
val JArray(hashes) = json.result \ "merkle"
|
||||
val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex((value)) }
|
||||
val blockHeight = intField(json.result, "block_height")
|
||||
val leaves = hashes collect { case JString(value) => BinaryData(value) }
|
||||
val blockHeight = longField(json.result, "block_height")
|
||||
val JInt(pos) = json.result \ "pos"
|
||||
GetMerkleResponse(txid, leaves, blockHeight, pos.toInt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def readServerAddresses(stream: InputStream): Seq[InetSocketAddress] = try {
|
||||
val JObject(values) = JsonMethods.parse(stream)
|
||||
val addresses = values.map {
|
||||
case (name, fields) =>
|
||||
val JString(port) = fields \ "t"
|
||||
new InetSocketAddress(name, port.toInt)
|
||||
}
|
||||
val randomized = Random.shuffle(addresses)
|
||||
randomized
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,240 +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.blockchain.electrum
|
||||
|
||||
import java.io.InputStream
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
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
|
||||
import org.json4s.JsonAST.{JObject, JString}
|
||||
import org.json4s.jackson.JsonMethods
|
||||
|
||||
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] {
|
||||
import ElectrumClientPool._
|
||||
|
||||
val statusListeners = collection.mutable.HashSet.empty[ActorRef]
|
||||
val addresses = collection.mutable.Map.empty[ActorRef, InetSocketAddress]
|
||||
|
||||
|
||||
// on startup, we attempt to connect to a number of electrum clients
|
||||
// they will send us an `ElectrumReady` message when they're connected, or
|
||||
// terminate if they cannot connect
|
||||
(0 until Math.min(MAX_CONNECTION_COUNT, serverAddresses.size)) foreach (_ => self ! Connect)
|
||||
|
||||
log.debug("starting electrum pool with serverAddresses={}", serverAddresses)
|
||||
|
||||
// custom supervision strategy: always stop Electrum clients when there's a problem, we will automatically reconnect
|
||||
// to another client
|
||||
override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(loggingEnabled = true) {
|
||||
case _ => SupervisorStrategy.stop
|
||||
}
|
||||
|
||||
startWith(Disconnected, DisconnectedData)
|
||||
|
||||
when(Disconnected) {
|
||||
case Event(ElectrumClient.ElectrumReady(height, tip, _), _) if addresses.contains(sender) =>
|
||||
sender ! ElectrumClient.HeaderSubscription(self)
|
||||
handleHeader(sender, height, tip, None)
|
||||
|
||||
case Event(ElectrumClient.AddStatusListener(listener), _) =>
|
||||
statusListeners += listener
|
||||
stay
|
||||
|
||||
case Event(Terminated(actor), _) =>
|
||||
log.info("lost connection to {}", addresses(actor))
|
||||
addresses -= actor
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, Connect)
|
||||
stay
|
||||
}
|
||||
|
||||
when(Connected) {
|
||||
case Event(ElectrumClient.ElectrumReady(height, tip, _), d: ConnectedData) if addresses.contains(sender) =>
|
||||
sender ! ElectrumClient.HeaderSubscription(self)
|
||||
handleHeader(sender, height, tip, Some(d))
|
||||
|
||||
case Event(ElectrumClient.HeaderSubscriptionResponse(height, tip), d: ConnectedData) if addresses.contains(sender) =>
|
||||
handleHeader(sender, height, tip, Some(d))
|
||||
|
||||
case Event(request: ElectrumClient.Request, ConnectedData(master, _)) =>
|
||||
master forward request
|
||||
stay
|
||||
|
||||
case Event(ElectrumClient.AddStatusListener(listener), d: ConnectedData) if addresses.contains(d.master) =>
|
||||
statusListeners += listener
|
||||
listener ! ElectrumClient.ElectrumReady(d.tips(d.master), addresses(d.master))
|
||||
stay
|
||||
|
||||
case Event(Terminated(actor), d: ConnectedData) =>
|
||||
val address = addresses(actor)
|
||||
addresses -= actor
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, Connect)
|
||||
val tips1 = d.tips - actor
|
||||
if (tips1.isEmpty) {
|
||||
log.info("lost connection to {}, no active connections left", address)
|
||||
goto(Disconnected) using DisconnectedData // no more connections
|
||||
} else if (d.master != actor) {
|
||||
log.info("lost connection to {}, we still have our master server", address)
|
||||
stay using d.copy(tips = tips1) // we don't care, this wasn't our master
|
||||
} else {
|
||||
log.info("lost connection to our master server {}", address)
|
||||
// we choose next best candidate as master
|
||||
val tips1 = d.tips - actor
|
||||
val (bestClient, bestTip) = tips1.toSeq.maxBy(_._2._1)
|
||||
handleHeader(bestClient, bestTip._1, bestTip._2, Some(d.copy(tips = tips1)))
|
||||
}
|
||||
}
|
||||
|
||||
whenUnhandled {
|
||||
case Event(Connect, _) =>
|
||||
pickAddress(serverAddresses, addresses.values.toSet) match {
|
||||
case Some(ElectrumServerAddress(address, ssl)) =>
|
||||
val resolved = new InetSocketAddress(address.getHostName, address.getPort)
|
||||
val client = context.actorOf(Props(new ElectrumClient(resolved, ssl)))
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
// we watch each electrum client, they will stop on disconnection
|
||||
context watch client
|
||||
addresses += (client -> address)
|
||||
case None => () // no more servers available
|
||||
}
|
||||
stay
|
||||
|
||||
case Event(ElectrumClient.ElectrumDisconnected, _) =>
|
||||
stay // ignored, we rely on Terminated messages to detect disconnections
|
||||
}
|
||||
|
||||
onTransition {
|
||||
case Connected -> Disconnected =>
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumDisconnected)
|
||||
context.system.eventStream.publish(ElectrumClient.ElectrumDisconnected)
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
||||
private def handleHeader(connection: ActorRef, height: Int, tip: BlockHeader, data: Option[ConnectedData]) = {
|
||||
val remoteAddress = addresses(connection)
|
||||
// we update our block count even if it doesn't come from our current master
|
||||
updateBlockCount(height)
|
||||
data match {
|
||||
case None =>
|
||||
// as soon as we have a connection to an electrum server, we select it as master
|
||||
log.info("selecting master {} at {}", remoteAddress, tip)
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
goto(Connected) using ConnectedData(connection, Map(connection -> (height, tip)))
|
||||
case Some(d) if connection != d.master && height >= d.blockHeight + 2L =>
|
||||
// we only switch to a new master if there is a significant difference with our current master, because
|
||||
// we don't want to switch to a new master every time a new block arrives (some servers will be notified before others)
|
||||
// we check that the current connection is not our master because on regtest when you generate several blocks at once
|
||||
// (and maybe on testnet in some pathological cases where there's a block every second) it may seen like our master
|
||||
// skipped a block and is suddenly at height + 2
|
||||
log.info("switching to master {} at {}", remoteAddress, tip)
|
||||
// we've switched to a new master, treat this as a disconnection/reconnection
|
||||
// so users (wallet, watcher, ...) will reset their subscriptions
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumDisconnected)
|
||||
context.system.eventStream.publish(ElectrumClient.ElectrumDisconnected)
|
||||
statusListeners.foreach(_ ! ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress))
|
||||
goto(Connected) using d.copy(master = connection, tips = d.tips + (connection -> (height, tip)))
|
||||
case Some(d) =>
|
||||
log.debug("received tip {} from {} at {}", tip, remoteAddress, height)
|
||||
stay using d.copy(tips = d.tips + (connection -> (height, tip)))
|
||||
}
|
||||
}
|
||||
|
||||
private def updateBlockCount(blockCount: Long): Unit = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
log.debug("current blockchain height={}", blockCount)
|
||||
context.system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ElectrumClientPool {
|
||||
|
||||
val MAX_CONNECTION_COUNT = 3
|
||||
|
||||
case class ElectrumServerAddress(adress: InetSocketAddress, ssl: SSL)
|
||||
|
||||
/**
|
||||
* Parses default electrum server list and extract addresses
|
||||
*
|
||||
* @param stream
|
||||
* @param sslEnabled select plaintext/ssl ports
|
||||
* @return
|
||||
*/
|
||||
def readServerAddresses(stream: InputStream, sslEnabled: Boolean): Set[ElectrumServerAddress] = try {
|
||||
val JObject(values) = JsonMethods.parse(stream)
|
||||
val addresses = values
|
||||
.toMap
|
||||
.filterKeys(!_.endsWith(".onion"))
|
||||
.flatMap {
|
||||
case (name, fields) =>
|
||||
if (sslEnabled) {
|
||||
// We don't authenticate seed servers (SSL.LOOSE), because:
|
||||
// - we don't know them so authentication doesn't really bring anything
|
||||
// - most of them have self-signed SSL certificates so it would always fail
|
||||
fields \ "s" match {
|
||||
case JString(port) => Some(ElectrumServerAddress(InetSocketAddress.createUnresolved(name, port.toInt), SSL.LOOSE))
|
||||
case _ => None
|
||||
}
|
||||
} else {
|
||||
fields \ "t" match {
|
||||
case JString(port) => Some(ElectrumServerAddress(InetSocketAddress.createUnresolved(name, port.toInt), SSL.OFF))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
addresses.toSet
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param serverAddresses all addresses to choose from
|
||||
* @param usedAddresses current connections
|
||||
* @return a random address that we're not connected to yet
|
||||
*/
|
||||
def pickAddress(serverAddresses: Set[ElectrumServerAddress], usedAddresses: Set[InetSocketAddress]): Option[ElectrumServerAddress] = {
|
||||
Random.shuffle(serverAddresses.filterNot(a => usedAddresses.contains(a.adress)).toSeq).headOption
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
sealed trait State
|
||||
case object Disconnected extends State
|
||||
case object Connected extends State
|
||||
|
||||
sealed trait Data
|
||||
case object DisconnectedData extends Data
|
||||
case class ConnectedData(master: ActorRef, tips: Map[ActorRef, (Int, BlockHeader)]) extends Data {
|
||||
def blockHeight = tips.get(master).map(_._1).getOrElse(0)
|
||||
}
|
||||
|
||||
case object Connect
|
||||
// @formatter:on
|
||||
}
|
||||
@ -1,46 +1,26 @@
|
||||
/*
|
||||
* 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.blockchain.electrum
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.pattern.ask
|
||||
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction, TxOut}
|
||||
import fr.acinq.eclair.addressToPublicKeyScript
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, OP_EQUAL, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction, TxOut}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
|
||||
import grizzled.slf4j.Logging
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implicit system: ActorSystem, ec: ExecutionContext, timeout: akka.util.Timeout) extends EclairWallet with Logging {
|
||||
class ElectrumEclairWallet(val wallet: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext, timeout: akka.util.Timeout) extends EclairWallet with Logging {
|
||||
|
||||
override def getBalance = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => balance.confirmed + balance.unconfirmed)
|
||||
|
||||
override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address)
|
||||
|
||||
def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse]
|
||||
|
||||
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long) = {
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0)
|
||||
(wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map(response => response match {
|
||||
case CompleteTransactionResponse(tx1, fee1, None) => MakeFundingTxResponse(tx1, 0, fee1)
|
||||
case CompleteTransactionResponse(_, _, Some(error)) => throw error
|
||||
case CompleteTransactionResponse(tx1, None) => MakeFundingTxResponse(tx1, 0)
|
||||
case CompleteTransactionResponse(_, Some(error)) => throw error
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,32 +46,24 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic
|
||||
}
|
||||
|
||||
def sendPayment(amount: Satoshi, address: String, feeRatePerKw: Long): Future[String] = {
|
||||
val publicKeyScript = Script.write(addressToPublicKeyScript(address, chainHash))
|
||||
val publicKeyScript = Base58Check.decode(address) match {
|
||||
case (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) => Script.pay2pkh(pubKeyHash)
|
||||
case (Base58.Prefix.ScriptAddressTestnet, scriptHash) => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil
|
||||
}
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0)
|
||||
|
||||
(wallet ? CompleteTransaction(tx, feeRatePerKw))
|
||||
.mapTo[CompleteTransactionResponse]
|
||||
.flatMap {
|
||||
case CompleteTransactionResponse(tx, _, None) => commit(tx).map {
|
||||
case CompleteTransactionResponse(tx, None) => commit(tx).map {
|
||||
case true => tx.txid.toString()
|
||||
case false => throw new RuntimeException(s"could not commit tx=$tx")
|
||||
case false => throw new RuntimeException(s"could not commit tx=${Transaction.write(tx)}")
|
||||
}
|
||||
case CompleteTransactionResponse(_, _, Some(error)) => throw error
|
||||
case CompleteTransactionResponse(_, Some(error)) => throw error
|
||||
}
|
||||
}
|
||||
|
||||
def sendAll(address: String, feeRatePerKw: Long): Future[(Transaction, Satoshi)] = {
|
||||
val publicKeyScript = Script.write(addressToPublicKeyScript(address, chainHash))
|
||||
(wallet ? SendAll(publicKeyScript, feeRatePerKw))
|
||||
.mapTo[SendAllResponse]
|
||||
.map {
|
||||
case SendAllResponse(tx, fee) => (tx, fee)
|
||||
}
|
||||
}
|
||||
def getMnemonics: Future[Seq[String]] = (wallet ? GetMnemonicCode).mapTo[GetMnemonicCodeResponse].map(_.mnemonics)
|
||||
|
||||
override def rollback(tx: Transaction): Future[Boolean] = (wallet ? CancelTransaction(tx)).map(_ => true)
|
||||
|
||||
override def doubleSpent(tx: Transaction): Future[Boolean] = {
|
||||
(wallet ? IsDoubleSpent(tx)).mapTo[IsDoubleSpentResponse].map(_.isDoubleSpent)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,31 +1,15 @@
|
||||
/*
|
||||
* 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.blockchain.electrum
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, Props, Stash, Terminated}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Script, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient._
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT, BITCOIN_PARENT_TX_CONFIRMED}
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.{Globals, ShortChannelId, TxCoordinates}
|
||||
import fr.acinq.eclair.{Globals, fromShortId}
|
||||
|
||||
import scala.collection.SortedMap
|
||||
|
||||
@ -35,46 +19,47 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
|
||||
override def unhandled(message: Any): Unit = message match {
|
||||
case ValidateRequest(c) =>
|
||||
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
|
||||
case c =>
|
||||
log.info(s"blindly validating channel=$c")
|
||||
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
|
||||
val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId)
|
||||
val (_, _, outputIndex) = fromShortId(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)))
|
||||
|
||||
IndividualResult(c, Some(fakeFundingTx), true)
|
||||
})
|
||||
case _ => log.warning(s"unhandled message $message")
|
||||
}
|
||||
|
||||
def receive = disconnected(Set.empty, Nil, SortedMap.empty)
|
||||
|
||||
def disconnected(watches: Set[Watch], publishQueue: Seq[PublishAsap], block2tx: SortedMap[Long, Seq[Transaction]]): Receive = {
|
||||
case ElectrumClient.ElectrumReady(_, _, _) =>
|
||||
case ElectrumClient.ElectrumReady =>
|
||||
client ! ElectrumClient.HeaderSubscription(self)
|
||||
case ElectrumClient.HeaderSubscriptionResponse(height, header) =>
|
||||
case ElectrumClient.HeaderSubscriptionResponse(header) =>
|
||||
watches.map(self ! _)
|
||||
publishQueue.map(self ! _)
|
||||
context become running(height, header, Set(), Map(), block2tx, Nil)
|
||||
context become running(header, Set(), Map(), block2tx, Nil)
|
||||
case watch: Watch => context become disconnected(watches + watch, publishQueue, block2tx)
|
||||
case publish: PublishAsap => context become disconnected(watches, publishQueue :+ publish, block2tx)
|
||||
}
|
||||
|
||||
def running(height: Int, tip: BlockHeader, watches: Set[Watch], scriptHashStatus: Map[ByteVector32, String], block2tx: SortedMap[Long, Seq[Transaction]], sent: Seq[Transaction]): Receive = {
|
||||
case ElectrumClient.HeaderSubscriptionResponse(newheight, newtip) if tip == newtip => ()
|
||||
def running(tip: ElectrumClient.Header, watches: Set[Watch], scriptHashStatus: Map[BinaryData, String], block2tx: SortedMap[Long, Seq[Transaction]], sent: Seq[Transaction]): Receive = {
|
||||
case ElectrumClient.HeaderSubscriptionResponse(newtip) if tip == newtip => ()
|
||||
|
||||
case ElectrumClient.HeaderSubscriptionResponse(newheight, newtip) =>
|
||||
log.info(s"new tip: ${newtip.blockId} $height")
|
||||
case ElectrumClient.HeaderSubscriptionResponse(newtip) =>
|
||||
log.info(s"new tip: ${newtip.block_hash} $newtip")
|
||||
watches collect {
|
||||
case watch: WatchConfirmed =>
|
||||
val scriptHash = computeScriptHash(watch.publicKeyScript)
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
}
|
||||
val toPublish = block2tx.filterKeys(_ <= newheight)
|
||||
val toPublish = block2tx.filterKeys(_ <= newtip.block_height)
|
||||
toPublish.values.flatten.foreach(tx => self ! PublishAsap(tx))
|
||||
context become running(newheight, newtip, watches, scriptHashStatus, block2tx -- toPublish.keys, sent)
|
||||
context become running(newtip, watches, scriptHashStatus, block2tx -- toPublish.keys, sent)
|
||||
|
||||
case watch: Watch if watches.contains(watch) => ()
|
||||
|
||||
@ -83,25 +68,25 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
log.info(s"added watch-spent on output=$txid:$outputIndex scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
|
||||
context.watch(watch.channel)
|
||||
context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case watch@WatchSpentBasic(_, txid, outputIndex, publicKeyScript, _) =>
|
||||
val scriptHash = computeScriptHash(publicKeyScript)
|
||||
log.info(s"added watch-spent-basic on output=$txid:$outputIndex scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
|
||||
context.watch(watch.channel)
|
||||
context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case watch@WatchConfirmed(_, txid, publicKeyScript, _, _) =>
|
||||
val scriptHash = computeScriptHash(publicKeyScript)
|
||||
log.info(s"added watch-confirmed on txid=$txid scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
context.watch(watch.channel)
|
||||
context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case Terminated(actor) =>
|
||||
val watches1 = watches.filterNot(_.channel == actor)
|
||||
context become running(height, tip, watches1, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches1, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status) =>
|
||||
scriptHashStatus.get(scriptHash) match {
|
||||
@ -111,33 +96,33 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
log.info(s"new status=$status for scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
}
|
||||
context become running(height, tip, watches, scriptHashStatus + (scriptHash -> status), block2tx, sent)
|
||||
context become running(tip, watches, scriptHashStatus + (scriptHash -> status), block2tx, sent)
|
||||
|
||||
case ElectrumClient.GetScriptHashHistoryResponse(_, history) =>
|
||||
// this is for WatchSpent/WatchSpentBasic
|
||||
history.filter(_.height >= 0).map(item => client ! ElectrumClient.GetTransaction(item.tx_hash))
|
||||
// this is for WatchConfirmed
|
||||
history.collect {
|
||||
case ElectrumClient.TransactionHistoryItem(txheight, tx_hash) if txheight > 0 => watches.collect {
|
||||
case ElectrumClient.TransactionHistoryItem(height, tx_hash) if height > 0 => watches.collect {
|
||||
case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx_hash =>
|
||||
val confirmations = height - txheight + 1
|
||||
log.info(s"txid=$txid was confirmed at height=$txheight and now has confirmations=$confirmations (currentHeight=${height})")
|
||||
val confirmations = tip.block_height - height + 1
|
||||
log.info(s"txid=$txid was confirmed at height=$height and now has confirmations=$confirmations (currentHeight=${tip.block_height})")
|
||||
if (confirmations >= minDepth) {
|
||||
// we need to get the tx position in the block
|
||||
client ! GetMerkle(tx_hash, txheight)
|
||||
client ! GetMerkle(tx_hash, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ElectrumClient.GetMerkleResponse(tx_hash, _, txheight, pos) =>
|
||||
val confirmations = height - txheight + 1
|
||||
case ElectrumClient.GetMerkleResponse(tx_hash, _, height, pos) =>
|
||||
val confirmations = tip.block_height - height + 1
|
||||
val triggered = watches.collect {
|
||||
case w@WatchConfirmed(channel, txid, _, minDepth, event) if txid == tx_hash && confirmations >= minDepth =>
|
||||
log.info(s"txid=$txid had confirmations=$confirmations in block=$txheight pos=$pos")
|
||||
channel ! WatchEventConfirmed(event, txheight.toInt, pos)
|
||||
log.info(s"txid=$txid had confirmations=$confirmations in block=$height pos=$pos")
|
||||
channel ! WatchEventConfirmed(event, height.toInt, pos)
|
||||
w
|
||||
}
|
||||
context become running(height, tip, watches -- triggered, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case ElectrumClient.GetTransactionResponse(spendingTx) =>
|
||||
val triggered = spendingTx.txIn.map(_.outPoint).flatMap(outPoint => watches.collect {
|
||||
@ -152,7 +137,7 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
channel ! WatchEventSpentBasic(event)
|
||||
Some(w)
|
||||
}).flatten
|
||||
context become running(height, tip, watches -- triggered, scriptHashStatus, block2tx, sent)
|
||||
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
@ -161,17 +146,17 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=$tx")
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
|
||||
val parentPublicKeyScript = WatchConfirmed.extractPublicKeyScript(tx.txIn.head.witness)
|
||||
self ! WatchConfirmed(self, parentTxid, parentPublicKeyScript, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
|
||||
} else if (cltvTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context become running(height, tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
context become running(tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
} else {
|
||||
log.info(s"publishing tx=$tx")
|
||||
log.info(s"publishing tx=${Transaction.write(tx)}")
|
||||
client ! BroadcastTransaction(tx)
|
||||
context become running(height, tip, watches, scriptHashStatus, block2tx, sent :+ tx)
|
||||
context become running(tip, watches, scriptHashStatus, block2tx, sent :+ tx)
|
||||
}
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
|
||||
@ -182,24 +167,24 @@ class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLoggi
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context become running(height, tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
context become running(tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
} else {
|
||||
log.info(s"publishing tx=$tx")
|
||||
log.info(s"publishing tx=${Transaction.write(tx)}")
|
||||
client ! BroadcastTransaction(tx)
|
||||
context become running(height, tip, watches, scriptHashStatus, block2tx, sent :+ tx)
|
||||
context become running(tip, watches, scriptHashStatus, block2tx, sent :+ tx)
|
||||
}
|
||||
|
||||
case ElectrumClient.BroadcastTransactionResponse(tx, error_opt) =>
|
||||
error_opt match {
|
||||
case None => log.info(s"broadcast succeeded for txid=${tx.txid} tx=$tx")
|
||||
case Some(error) if error.message.contains("transaction already in block chain") => log.info(s"broadcast ignored for txid=${tx.txid} tx=$tx (tx was already in blockchain)")
|
||||
case Some(error) => log.error(s"broadcast failed for txid=${tx.txid} tx=$tx with error=$error")
|
||||
case None => log.info(s"broadcast succeeded for txid=${tx.txid} tx=${Transaction.write(tx)}")
|
||||
case Some(error) if error.message.contains("transaction already in block chain") => log.info(s"broadcast ignored for txid=${tx.txid} tx=${Transaction.write(tx)} (tx was already in blockchain)")
|
||||
case Some(error) => log.error(s"broadcast failed for txid=${tx.txid} tx=${Transaction.write(tx)} with error=$error")
|
||||
}
|
||||
context become running(height, tip, watches, scriptHashStatus, block2tx, sent diff Seq(tx))
|
||||
context become running(tip, watches, scriptHashStatus, block2tx, sent diff Seq(tx))
|
||||
|
||||
case ElectrumClient.ElectrumDisconnected =>
|
||||
// we remember watches and keep track of tx that have not yet been published
|
||||
// we also re-send the txes that we previously sent but hadn't yet received the confirmation
|
||||
// we also re-send the txes that we previsouly sent but hadn't yet received the confirmation
|
||||
context become disconnected(watches, sent.map(PublishAsap(_)), block2tx)
|
||||
}
|
||||
|
||||
@ -208,10 +193,10 @@ 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")
|
||||
val serverAddresses = Seq(new InetSocketAddress("localhost", 51000), new InetSocketAddress("localhost", 51001))
|
||||
val client = context.actorOf(Props(new ElectrumClient(serverAddresses)), "client")
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
|
||||
override def unhandled(message: Any): Unit = {
|
||||
@ -220,7 +205,7 @@ object ElectrumWatcher extends App {
|
||||
}
|
||||
|
||||
def receive = {
|
||||
case ElectrumClient.ElectrumReady(_, _, _) =>
|
||||
case ElectrumClient.ElectrumReady =>
|
||||
log.info(s"starting watcher")
|
||||
context become running(context.actorOf(Props(new ElectrumWatcher(client)), "watcher"))
|
||||
}
|
||||
|
||||
@ -1,41 +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.blockchain.electrum.db
|
||||
|
||||
import fr.acinq.bitcoin.{BlockHeader, ByteVector32}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData
|
||||
|
||||
trait HeaderDb {
|
||||
def addHeader(height: Int, header: BlockHeader): Unit
|
||||
|
||||
def addHeaders(startHeight: Int, headers: Seq[BlockHeader]): Unit
|
||||
|
||||
def getHeader(height: Int): Option[BlockHeader]
|
||||
|
||||
// used only in unit tests
|
||||
def getHeader(blockHash: ByteVector32): Option[(Int, BlockHeader)]
|
||||
|
||||
def getHeaders(startHeight: Int, maxCount: Option[Int]): Seq[BlockHeader]
|
||||
|
||||
def getTip: Option[(Int, BlockHeader)]
|
||||
}
|
||||
|
||||
trait WalletDb extends HeaderDb {
|
||||
def persist(data: PersistentData): Unit
|
||||
|
||||
def readPersistentData(): Option[PersistentData]
|
||||
}
|
||||
@ -1,219 +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.blockchain.electrum.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Transaction}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{GetMerkleResponse, TransactionHistoryItem}
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData
|
||||
import fr.acinq.eclair.blockchain.electrum.db.WalletDb
|
||||
import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumWallet}
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
|
||||
class SqliteWalletDb(sqlite: Connection) extends WalletDb {
|
||||
|
||||
import SqliteUtils._
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS headers (height INTEGER NOT NULL PRIMARY KEY, block_hash BLOB NOT NULL, header BLOB NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS wallet (data BLOB)")
|
||||
}
|
||||
|
||||
override def addHeader(height: Int, header: BlockHeader): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)")) { statement =>
|
||||
statement.setInt(1, height)
|
||||
statement.setBytes(2, header.hash.toArray)
|
||||
statement.setBytes(3, BlockHeader.write(header).toArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override def addHeaders(startHeight: Int, headers: Seq[BlockHeader]): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)"), disableAutoCommit = true) { statement =>
|
||||
var height = startHeight
|
||||
headers.foreach(header => {
|
||||
statement.setInt(1, height)
|
||||
statement.setBytes(2, header.hash.toArray)
|
||||
statement.setBytes(3, BlockHeader.write(header).toArray)
|
||||
statement.addBatch()
|
||||
height = height + 1
|
||||
})
|
||||
val result = statement.executeBatch()
|
||||
}
|
||||
}
|
||||
|
||||
override def getHeader(height: Int): Option[BlockHeader] = {
|
||||
using(sqlite.prepareStatement("SELECT header FROM headers WHERE height = ?")) { statement =>
|
||||
statement.setInt(1, height)
|
||||
val rs = statement.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some(BlockHeader.read(rs.getBytes("header")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def getHeader(blockHash: ByteVector32): Option[(Int, BlockHeader)] = {
|
||||
using(sqlite.prepareStatement("SELECT height, header FROM headers WHERE block_hash = ?")) { statement =>
|
||||
statement.setBytes(1, blockHash.toArray)
|
||||
val rs = statement.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some((rs.getInt("height"), BlockHeader.read(rs.getBytes("header"))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def getHeaders(startHeight: Int, maxCount: Option[Int]): Seq[BlockHeader] = {
|
||||
val query = "SELECT height, header FROM headers WHERE height >= ? ORDER BY height " + maxCount.map(m => s" LIMIT $m").getOrElse("")
|
||||
using(sqlite.prepareStatement(query)) { statement =>
|
||||
statement.setInt(1, startHeight)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[BlockHeader] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ BlockHeader.read(rs.getBytes("header"))
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override def getTip: Option[(Int, BlockHeader)] = {
|
||||
using(sqlite.prepareStatement("SELECT t.height, t.header FROM headers t INNER JOIN (SELECT MAX(height) AS maxHeight FROM headers) q ON t.height = q.maxHeight")) { statement =>
|
||||
val rs = statement.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some((rs.getInt("height"), BlockHeader.read(rs.getBytes("header"))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def persist(data: ElectrumWallet.PersistentData): Unit = {
|
||||
val bin = SqliteWalletDb.serialize(data)
|
||||
using(sqlite.prepareStatement("UPDATE wallet SET data=(?)")) { update =>
|
||||
update.setBytes(1, bin)
|
||||
if (update.executeUpdate() == 0) {
|
||||
using(sqlite.prepareStatement("INSERT INTO wallet VALUES (?)")) { statement =>
|
||||
statement.setBytes(1, bin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def readPersistentData(): Option[ElectrumWallet.PersistentData] = {
|
||||
using(sqlite.prepareStatement("SELECT data FROM wallet")) { statement =>
|
||||
val rs = statement.executeQuery()
|
||||
if (rs.next()) {
|
||||
Option(rs.getBytes(1)).map(bin => SqliteWalletDb.deserializePersistentData(bin))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SqliteWalletDb {
|
||||
|
||||
import fr.acinq.eclair.wire.ChannelCodecs._
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs._
|
||||
import scodec.Codec
|
||||
import scodec.bits.BitVector
|
||||
import scodec.codecs._
|
||||
|
||||
val proofCodec: Codec[GetMerkleResponse] = (
|
||||
("txid" | bytes32) ::
|
||||
("merkle" | listOfN(uint16, bytes32)) ::
|
||||
("block_height" | uint24) ::
|
||||
("pos" | uint24)).as[GetMerkleResponse]
|
||||
|
||||
def serializeMerkleProof(proof: GetMerkleResponse): Array[Byte] = proofCodec.encode(proof).require.toByteArray
|
||||
|
||||
def deserializeMerkleProof(bin: Array[Byte]): GetMerkleResponse = proofCodec.decode(BitVector(bin)).require.value
|
||||
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs._
|
||||
|
||||
val statusListCodec: Codec[List[(ByteVector32, String)]] = listOfN(uint16, bytes32 ~ cstring)
|
||||
|
||||
val statusCodec: Codec[Map[ByteVector32, String]] = Codec[Map[ByteVector32, String]](
|
||||
(map: Map[ByteVector32, String]) => statusListCodec.encode(map.toList),
|
||||
(wire: BitVector) => statusListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
val heightsListCodec: Codec[List[(ByteVector32, Int)]] = listOfN(uint16, bytes32 ~ int32)
|
||||
|
||||
val heightsCodec: Codec[Map[ByteVector32, Int]] = Codec[Map[ByteVector32, Int]](
|
||||
(map: Map[ByteVector32, Int]) => heightsListCodec.encode(map.toList),
|
||||
(wire: BitVector) => heightsListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
val transactionListCodec: Codec[List[(ByteVector32, Transaction)]] = listOfN(uint16, bytes32 ~ txCodec)
|
||||
|
||||
val transactionsCodec: Codec[Map[ByteVector32, Transaction]] = Codec[Map[ByteVector32, Transaction]](
|
||||
(map: Map[ByteVector32, Transaction]) => transactionListCodec.encode(map.toList),
|
||||
(wire: BitVector) => transactionListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
val transactionHistoryItemCodec: Codec[ElectrumClient.TransactionHistoryItem] = (
|
||||
("height" | int32) :: ("tx_hash" | bytes32)).as[ElectrumClient.TransactionHistoryItem]
|
||||
|
||||
val seqOfTransactionHistoryItemCodec: Codec[List[TransactionHistoryItem]] = listOfN[TransactionHistoryItem](uint16, transactionHistoryItemCodec)
|
||||
|
||||
val historyListCodec: Codec[List[(ByteVector32, List[ElectrumClient.TransactionHistoryItem])]] =
|
||||
listOfN[(ByteVector32, List[ElectrumClient.TransactionHistoryItem])](uint16, bytes32 ~ seqOfTransactionHistoryItemCodec)
|
||||
|
||||
val historyCodec: Codec[Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]] = Codec[Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]](
|
||||
(map: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]) => historyListCodec.encode(map.toList),
|
||||
(wire: BitVector) => historyListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
val proofsListCodec: Codec[List[(ByteVector32, GetMerkleResponse)]] = listOfN(uint16, bytes32 ~ proofCodec)
|
||||
|
||||
val proofsCodec: Codec[Map[ByteVector32, GetMerkleResponse]] = Codec[Map[ByteVector32, GetMerkleResponse]](
|
||||
(map: Map[ByteVector32, GetMerkleResponse]) => proofsListCodec.encode(map.toList),
|
||||
(wire: BitVector) => proofsListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
/**
|
||||
* change this value
|
||||
* -if the new codec is incompatible with the old one
|
||||
* - OR if you want to force a full sync from Electrum servers
|
||||
*/
|
||||
val version = 0x0000
|
||||
|
||||
val persistentDataCodec: Codec[PersistentData] = (
|
||||
("version" | constant(BitVector.fromInt(version))) ::
|
||||
("accountKeysCount" | int32) ::
|
||||
("changeKeysCount" | int32) ::
|
||||
("status" | statusCodec) ::
|
||||
("transactions" | transactionsCodec) ::
|
||||
("heights" | heightsCodec) ::
|
||||
("history" | historyCodec) ::
|
||||
("proofs" | proofsCodec) ::
|
||||
("pendingTransactions" | listOfN(uint16, txCodec)) ::
|
||||
("locks" | provide(Set.empty[Transaction]))).as[PersistentData]
|
||||
|
||||
def serialize(data: PersistentData): Array[Byte] = persistentDataCodec.encode(data).require.toByteArray
|
||||
|
||||
def deserializePersistentData(bin: Array[Byte]): PersistentData = persistentDataCodec.decode(BitVector(bin)).require.value
|
||||
}
|
||||
@ -1,80 +1,44 @@
|
||||
/*
|
||||
* 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.blockchain.fee
|
||||
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.bitcoin.Btc
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST._
|
||||
import org.json4s.JsonAST.{JDouble, JInt}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerKB)(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
implicit val formats = DefaultFormats.withBigDecimal
|
||||
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerates: FeeratesPerByte)(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return the current
|
||||
*/
|
||||
def estimateSmartFee(nBlocks: Int): Future[Long] =
|
||||
rpcClient.invoke("estimatesmartfee", nBlocks).map(BitcoinCoreFeeProvider.parseFeeEstimate)
|
||||
rpcClient.invoke("estimatesmartfee", nBlocks).map(json => {
|
||||
json \ "feerate" match {
|
||||
case JDouble(feerate) => Btc(feerate).toLong
|
||||
case JInt(feerate) if feerate.toLong < 0 => feerate.toLong
|
||||
case JInt(feerate) => Btc(feerate.toLong).toLong
|
||||
}
|
||||
})
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] = for {
|
||||
override def getFeerates: Future[FeeratesPerByte] = for {
|
||||
block_1 <- estimateSmartFee(1)
|
||||
blocks_2 <- estimateSmartFee(2)
|
||||
blocks_6 <- estimateSmartFee(6)
|
||||
blocks_12 <- estimateSmartFee(12)
|
||||
blocks_36 <- estimateSmartFee(36)
|
||||
blocks_72 <- estimateSmartFee(72)
|
||||
} yield FeeratesPerKB(
|
||||
} yield FeeratesPerByte(
|
||||
block_1 = if (block_1 > 0) block_1 else defaultFeerates.block_1,
|
||||
blocks_2 = if (blocks_2 > 0) blocks_2 else defaultFeerates.blocks_2,
|
||||
blocks_6 = if (blocks_6 > 0) blocks_6 else defaultFeerates.blocks_6,
|
||||
blocks_12 = if (blocks_12 > 0) blocks_12 else defaultFeerates.blocks_12,
|
||||
blocks_36 = if (blocks_36 > 0) blocks_36 else defaultFeerates.blocks_36,
|
||||
blocks_72 = if (blocks_72 > 0) blocks_72 else defaultFeerates.blocks_72)
|
||||
}
|
||||
|
||||
object BitcoinCoreFeeProvider {
|
||||
def parseFeeEstimate(json: JValue): Long = {
|
||||
json \ "errors" match {
|
||||
case JNothing =>
|
||||
json \ "feerate" match {
|
||||
case JDecimal(feerate) =>
|
||||
// estimatesmartfee returns a fee rate in Btc/KB
|
||||
btc2satoshi(Btc(feerate)).amount
|
||||
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
|
||||
}
|
||||
case JArray(errors) =>
|
||||
val error = errors collect { case JString(error) => error } mkString (", ")
|
||||
throw new RuntimeException(s"estimatesmartfee failed: $error")
|
||||
case _ =>
|
||||
throw new RuntimeException("estimatesmartfee failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,77 +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.blockchain.fee
|
||||
|
||||
import com.softwaremill.sttp._
|
||||
import com.softwaremill.sttp.json4s._
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.{JInt, JValue}
|
||||
import org.json4s.jackson.Serialization
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class BitgoFeeProvider(chainHash: ByteVector32)(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
import BitgoFeeProvider._
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
implicit val serialization = Serialization
|
||||
|
||||
val uri = chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash => uri"https://www.bitgo.com/api/v2/btc/tx/fee"
|
||||
case _ => uri"https://test.bitgo.com/api/v2/tbtc/tx/fee"
|
||||
}
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] =
|
||||
for {
|
||||
res <- sttp.get(uri)
|
||||
.response(asJson[JValue])
|
||||
.send()
|
||||
feeRanges = parseFeeRanges(res.unsafeBody)
|
||||
} yield extractFeerates(feeRanges)
|
||||
}
|
||||
|
||||
object BitgoFeeProvider {
|
||||
|
||||
case class BlockTarget(block: Int, fee: Long)
|
||||
|
||||
def parseFeeRanges(json: JValue): Seq[BlockTarget] = {
|
||||
val blockTargets = json \ "feeByBlockTarget"
|
||||
blockTargets.foldField(Seq.empty[BlockTarget]) {
|
||||
// BitGo returns estimates in Satoshi/KB, which is what we want
|
||||
case (list, (strBlockTarget, JInt(feePerKB))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKB.longValue())
|
||||
}
|
||||
}
|
||||
|
||||
def extractFeerate(feeRanges: Seq[BlockTarget], maxBlockDelay: Int): Long = {
|
||||
// first we keep only fee ranges with a max block delay below the limit
|
||||
val belowLimit = feeRanges.filter(_.block <= maxBlockDelay)
|
||||
// out of all the remaining fee ranges, we select the one with the minimum higher bound
|
||||
belowLimit.map(_.fee).min
|
||||
}
|
||||
|
||||
def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerKB =
|
||||
FeeratesPerKB(
|
||||
block_1 = extractFeerate(feeRanges, 1),
|
||||
blocks_2 = extractFeerate(feeRanges, 2),
|
||||
blocks_6 = extractFeerate(feeRanges, 6),
|
||||
blocks_12 = extractFeerate(feeRanges, 12),
|
||||
blocks_36 = extractFeerate(feeRanges, 36),
|
||||
blocks_72 = extractFeerate(feeRanges, 72))
|
||||
|
||||
}
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.blockchain.fee
|
||||
|
||||
import scala.concurrent.Future
|
||||
@ -21,8 +5,8 @@ import scala.concurrent.Future
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class ConstantFeeProvider(feerates: FeeratesPerKB) extends FeeProvider {
|
||||
class ConstantFeeProvider(feerates: FeeratesPerByte) extends FeeProvider {
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] = Future.successful(feerates)
|
||||
override def getFeerates: Future[FeeratesPerByte] = Future.successful(feerates)
|
||||
|
||||
}
|
||||
|
||||
@ -1,47 +1,33 @@
|
||||
/*
|
||||
* 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.blockchain.fee
|
||||
|
||||
import com.softwaremill.sttp._
|
||||
import com.softwaremill.sttp.json4s._
|
||||
import org.json4s.DefaultFormats
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.stream.ActorMaterializer
|
||||
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
|
||||
import org.json4s.JsonAST.{JArray, JInt, JValue}
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.json4s.{DefaultFormats, jackson}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 16/11/2017.
|
||||
*/
|
||||
class EarnDotComFeeProvider(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider {
|
||||
class EarnDotComFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
import EarnDotComFeeProvider._
|
||||
|
||||
implicit val materializer = ActorMaterializer()
|
||||
val httpClient = Http(system)
|
||||
implicit val serialization = jackson.Serialization
|
||||
implicit val formats = DefaultFormats
|
||||
implicit val serialization = Serialization
|
||||
|
||||
val uri = uri"https://bitcoinfees.earn.com/api/v1/fees/list"
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] =
|
||||
override def getFeerates: Future[FeeratesPerByte] =
|
||||
for {
|
||||
json <- sttp.get(uri)
|
||||
.response(asJson[JValue])
|
||||
.send()
|
||||
feeRanges = parseFeeRanges(json.unsafeBody)
|
||||
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://bitcoinfees.earn.com/api/v1/fees/list"), method = HttpMethods.GET))
|
||||
json <- Unmarshal(httpRes).to[JValue]
|
||||
feeRanges = parseFeeRanges(json)
|
||||
} yield extractFeerates(feeRanges)
|
||||
}
|
||||
|
||||
@ -57,20 +43,19 @@ object EarnDotComFeeProvider {
|
||||
val JInt(memCount) = item \ "memCount"
|
||||
val JInt(minDelay) = item \ "minDelay"
|
||||
val JInt(maxDelay) = item \ "maxDelay"
|
||||
// earn.com returns fees in Satoshi/byte and we want Satoshi/KiloByte
|
||||
FeeRange(minFee = 1000 * minFee.toLong, maxFee = 1000 * maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong)
|
||||
FeeRange(minFee = minFee.toLong, maxFee = maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong)
|
||||
})
|
||||
}
|
||||
|
||||
def extractFeerate(feeRanges: Seq[FeeRange], maxBlockDelay: Int): Long = {
|
||||
// first we keep only fee ranges with a max block delay below the limit
|
||||
val belowLimit = feeRanges.filter(_.maxDelay <= maxBlockDelay)
|
||||
// out of all the remaining fee ranges, we select the one with the minimum higher bound and make sure it is > 0
|
||||
Math.max(belowLimit.minBy(_.maxFee).maxFee, 1)
|
||||
// out of all the remaining fee ranges, we select the one with the minimum higher bound
|
||||
belowLimit.minBy(_.maxFee).maxFee
|
||||
}
|
||||
|
||||
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerKB =
|
||||
FeeratesPerKB(
|
||||
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerByte =
|
||||
FeeratesPerByte(
|
||||
block_1 = extractFeerate(feeRanges, 1),
|
||||
blocks_2 = extractFeerate(feeRanges, 2),
|
||||
blocks_6 = extractFeerate(feeRanges, 6),
|
||||
|
||||
@ -1,53 +1,20 @@
|
||||
/*
|
||||
* 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.blockchain.fee
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* This provider will try all child providers in sequence, until one of them works
|
||||
*
|
||||
* @param providers a sequence of providers; they will be tried one after the others until one of them succeeds
|
||||
* @param minFeeratePerByte a configurable minimum value for feerates
|
||||
*/
|
||||
class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: Long)(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
class FallbackFeeProvider(providers: Seq[FeeProvider])(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
require(providers.size >= 1, "need at least one fee provider")
|
||||
require(minFeeratePerByte > 0, "minimum fee rate must be strictly greater than 0")
|
||||
|
||||
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerKB] =
|
||||
def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerByte] =
|
||||
fallbacks match {
|
||||
case last +: Nil => last.getFeerates
|
||||
case head +: remaining => head.getFeerates.recoverWith { case _ => getFeerates(remaining) }
|
||||
}
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] = getFeerates(providers).map(FallbackFeeProvider.enforceMinimumFeerate(_, minFeeratePerByte))
|
||||
|
||||
}
|
||||
|
||||
object FallbackFeeProvider {
|
||||
|
||||
def enforceMinimumFeerate(feeratesPerKB: FeeratesPerKB, minFeeratePerByte: Long) : FeeratesPerKB = feeratesPerKB.copy(
|
||||
block_1 = Math.max(feeratesPerKB.block_1, minFeeratePerByte * 1000),
|
||||
blocks_2 = Math.max(feeratesPerKB.blocks_2, minFeeratePerByte * 1000),
|
||||
blocks_6 = Math.max(feeratesPerKB.blocks_6, minFeeratePerByte * 1000),
|
||||
blocks_12 = Math.max(feeratesPerKB.blocks_12, minFeeratePerByte * 1000),
|
||||
blocks_36 = Math.max(feeratesPerKB.blocks_36, minFeeratePerByte * 1000),
|
||||
blocks_72 = Math.max(feeratesPerKB.blocks_72, minFeeratePerByte * 1000)
|
||||
)
|
||||
override def getFeerates: Future[FeeratesPerByte] = getFeerates(providers)
|
||||
|
||||
}
|
||||
|
||||
@ -1,22 +1,6 @@
|
||||
/*
|
||||
* 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.blockchain.fee
|
||||
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.feerateByte2Kw
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
@ -25,28 +9,22 @@ import scala.concurrent.Future
|
||||
*/
|
||||
trait FeeProvider {
|
||||
|
||||
def getFeerates: Future[FeeratesPerKB]
|
||||
def getFeerates: Future[FeeratesPerByte]
|
||||
|
||||
}
|
||||
|
||||
// stores fee rate in satoshi/kb (1 kb = 1000 bytes)
|
||||
case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
|
||||
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
|
||||
}
|
||||
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
|
||||
|
||||
// stores fee rate in satoshi/kw (1 kw = 1000 weight units)
|
||||
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long) {
|
||||
require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0, "all feerates must be strictly greater than 0")
|
||||
}
|
||||
case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
|
||||
|
||||
object FeeratesPerKw {
|
||||
def apply(feerates: FeeratesPerKB): FeeratesPerKw = FeeratesPerKw(
|
||||
block_1 = feerateKB2Kw(feerates.block_1),
|
||||
blocks_2 = feerateKB2Kw(feerates.blocks_2),
|
||||
blocks_6 = feerateKB2Kw(feerates.blocks_6),
|
||||
blocks_12 = feerateKB2Kw(feerates.blocks_12),
|
||||
blocks_36 = feerateKB2Kw(feerates.blocks_36),
|
||||
blocks_72 = feerateKB2Kw(feerates.blocks_72))
|
||||
def apply(feerates: FeeratesPerByte): FeeratesPerKw = FeeratesPerKw(
|
||||
block_1 = feerateByte2Kw(feerates.block_1),
|
||||
blocks_2 = feerateByte2Kw(feerates.blocks_2),
|
||||
blocks_6 = feerateByte2Kw(feerates.blocks_6),
|
||||
blocks_12 = feerateByte2Kw(feerates.blocks_12),
|
||||
blocks_36 = feerateByte2Kw(feerates.blocks_36),
|
||||
blocks_72 = feerateByte2Kw(feerates.blocks_72))
|
||||
|
||||
/**
|
||||
* Used in tests
|
||||
@ -62,4 +40,3 @@ object FeeratesPerKw {
|
||||
blocks_36 = feeratePerKw,
|
||||
blocks_72 = feeratePerKw)
|
||||
}
|
||||
|
||||
|
||||
@ -1,51 +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.blockchain.fee
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class SmoothFeeProvider(provider: FeeProvider, windowSize: Int)(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
require(windowSize > 0)
|
||||
|
||||
var queue = List.empty[FeeratesPerKB]
|
||||
|
||||
def append(rate: FeeratesPerKB): Unit = synchronized {
|
||||
queue = queue :+ rate
|
||||
if (queue.length > windowSize) queue = queue.drop(1)
|
||||
}
|
||||
|
||||
override def getFeerates: Future[FeeratesPerKB] = {
|
||||
for {
|
||||
rate <- provider.getFeerates
|
||||
_ = append(rate)
|
||||
} yield SmoothFeeProvider.smooth(queue)
|
||||
}
|
||||
}
|
||||
|
||||
object SmoothFeeProvider {
|
||||
|
||||
def avg(i: Seq[Long]): Long = i.sum / i.size
|
||||
|
||||
def smooth(rates: Seq[FeeratesPerKB]): FeeratesPerKB =
|
||||
FeeratesPerKB(
|
||||
block_1 = avg(rates.map(_.block_1)),
|
||||
blocks_2 = avg(rates.map(_.blocks_2)),
|
||||
blocks_6 = avg(rates.map(_.blocks_6)),
|
||||
blocks_12 = avg(rates.map(_.blocks_12)),
|
||||
blocks_36 = avg(rates.map(_.blocks_36)),
|
||||
blocks_72 = avg(rates.map(_.blocks_72)))
|
||||
}
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,27 +1,8 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.channel.Channel.ChannelError
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}
|
||||
|
||||
/**
|
||||
* Created by PM on 17/08/2016.
|
||||
@ -29,33 +10,14 @@ import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate}
|
||||
|
||||
trait ChannelEvent
|
||||
|
||||
case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32) extends ChannelEvent
|
||||
case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: BinaryData) extends ChannelEvent
|
||||
|
||||
case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: ByteVector32, currentData: HasCommitments) extends ChannelEvent
|
||||
case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: BinaryData, currentData: HasCommitments) extends ChannelEvent
|
||||
|
||||
case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent
|
||||
case class ChannelIdAssigned(channel: ActorRef, temporaryChannelId: BinaryData, channelId: BinaryData) extends ChannelEvent
|
||||
|
||||
case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId) extends ChannelEvent
|
||||
|
||||
case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey) extends ChannelEvent
|
||||
case class ShortChannelIdAssigned(channel: ActorRef, channelId: BinaryData, shortChannelId: Long) extends ChannelEvent
|
||||
|
||||
case class ChannelStateChanged(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
|
||||
|
||||
case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class ChannelFailed(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, data: Data, error: ChannelError) 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 ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: Data) extends ChannelEvent
|
||||
|
||||
case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, refundAtBlock: Long) extends ChannelEvent
|
||||
|
||||
case class ChannelClosed(channel: ActorRef, channelId: ByteVector32, closeType: String, commitments: Commitments)
|
||||
case class ChannelSignatureReceived(channel: ActorRef, Commitments: Commitments) extends ChannelEvent
|
||||
|
||||
@ -1,84 +1,43 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.Scalar
|
||||
import fr.acinq.bitcoin.{ByteVector32, Transaction}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.payment.Origin
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc}
|
||||
|
||||
/**
|
||||
* Created by PM on 11/04/2017.
|
||||
*/
|
||||
|
||||
class ChannelException(val channelId: ByteVector32, message: String) extends RuntimeException(message)
|
||||
class ChannelException(channelId: BinaryData, 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 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 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")
|
||||
case class CannotCloseWithUnsignedOutgoingHtlcs(override val channelId: ByteVector32) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
|
||||
case class ChannelUnavailable (override val channelId: ByteVector32) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
|
||||
case class InvalidFinalScript (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid final script")
|
||||
case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out")
|
||||
case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}")
|
||||
case class HtlcTimedout (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids
|
||||
case class HtlcOverridenByLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, "htlc was overriden by local commit")
|
||||
case class FeerateTooSmall (override val channelId: ByteVector32, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"remote fee rate is too small: remoteFeeratePerKw=$remoteFeeratePerKw")
|
||||
case class FeerateTooDifferent (override val channelId: ByteVector32, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
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 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 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 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 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")
|
||||
case class InvalidRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid revocation")
|
||||
case class InvalidRevokedCommitProof (override val channelId: ByteVector32, ourCommitmentNumber: Long, theirCommitmentNumber: Long, perCommitmentSecret: Scalar) extends ChannelException(channelId, s"counterparty claimed that we have a revoked commit but their proof doesn't check out: ourCommitmentNumber=$ourCommitmentNumber theirCommitmentNumber=$theirCommitmentNumber perCommitmentSecret=$perCommitmentSecret")
|
||||
case class CommitmentSyncError (override val channelId: ByteVector32) extends ChannelException(channelId, "commitment sync error")
|
||||
case class RevocationSyncError (override val channelId: ByteVector32) extends ChannelException(channelId, "revocation sync error")
|
||||
case class InvalidFailureCode (override val channelId: ByteVector32) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
|
||||
case class PleasePublishYourCommitment (override val channelId: ByteVector32) extends ChannelException(channelId, "please publish your local commitment")
|
||||
case class AddHtlcFailed (override val channelId: ByteVector32, paymentHash: ByteVector32, t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate], originalCommand: Option[CMD_ADD_HTLC]) extends ChannelException(channelId, s"cannot add htlc with origin=$origin reason=${t.getMessage}")
|
||||
case class CommandUnavailableInThisState (override val channelId: ByteVector32, command: String, state: State) extends ChannelException(channelId, s"cannot execute command=$command in state=$state")
|
||||
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
|
||||
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
|
||||
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
|
||||
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
|
||||
case class CannotCloseWithUnsignedOutgoingHtlcs(channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
|
||||
case class ChannelUnavailable (channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
|
||||
case class InvalidFinalScript (channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
|
||||
case class HtlcTimedout (channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
|
||||
case class FeerateTooDifferent (channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
case class InvalidCloseSignature (channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
|
||||
case class InvalidCommitmentSignature (channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
|
||||
case class ForcedLocalCommit (channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
|
||||
case class UnexpectedHtlcId (channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
|
||||
case class InvalidPaymentHash (channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
|
||||
case class ExpiryTooSmall (channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
|
||||
case class ExpiryCannotBeInThePast (channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
|
||||
case class HtlcValueTooSmall (channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
|
||||
case class HtlcValueTooHighInFlight (channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
|
||||
case class TooManyAcceptedHtlcs (channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
|
||||
case class InsufficientFunds (channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
|
||||
case class InvalidHtlcPreimage (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
|
||||
case class UnknownHtlcId (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
|
||||
case class FundeeCannotSendUpdateFee (channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
|
||||
case class CannotAffordFees (channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
|
||||
case class CannotSignWithoutChanges (channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
|
||||
case class CannotSignBeforeRevocation (channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
|
||||
case class UnexpectedRevocation (channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
|
||||
case class InvalidRevocation (channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
|
||||
case class CommitmentSyncError (channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
|
||||
case class RevocationSyncError (channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
|
||||
case class InvalidFailureCode (channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
|
||||
// @formatter:on
|
||||
@ -1,32 +1,13 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey}
|
||||
import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction}
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction}
|
||||
import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
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, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{ShortChannelId, UInt64}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
|
||||
|
||||
/**
|
||||
@ -52,6 +33,7 @@ case object WAIT_FOR_ACCEPT_CHANNEL extends State
|
||||
case object WAIT_FOR_FUNDING_INTERNAL extends State
|
||||
case object WAIT_FOR_FUNDING_CREATED extends State
|
||||
case object WAIT_FOR_FUNDING_SIGNED extends State
|
||||
case object WAIT_FOR_FUNDING_PUBLISHED extends State
|
||||
case object WAIT_FOR_FUNDING_CONFIRMED extends State
|
||||
case object WAIT_FOR_FUNDING_LOCKED extends State
|
||||
case object NORMAL extends State
|
||||
@ -61,7 +43,7 @@ case object CLOSING extends State
|
||||
case object CLOSED extends State
|
||||
case object OFFLINE extends State
|
||||
case object SYNCING extends State
|
||||
case object WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT extends State
|
||||
case object ERR_FUNDING_PUBLISH_FAILED extends State
|
||||
case object ERR_FUNDING_LOST extends State
|
||||
case object ERR_FUNDING_TIMEOUT extends State
|
||||
case object ERR_INFORMATION_LEAK extends State
|
||||
@ -77,11 +59,12 @@ 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_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init)
|
||||
case class INPUT_INIT_FUNDER(temporaryChannelId: BinaryData, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte)
|
||||
case class INPUT_INIT_FUNDEE(temporaryChannelId: BinaryData, 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_PUBLISH_LOCALCOMMIT // used in tests
|
||||
case object INPUT_DISCONNECTED
|
||||
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
|
||||
case class INPUT_RECONNECTED(remote: ActorRef)
|
||||
case class INPUT_RESTORED(data: HasCommitments)
|
||||
|
||||
sealed trait BitcoinEvent
|
||||
@ -93,7 +76,7 @@ case object BITCOIN_FUNDING_TIMEOUT extends BitcoinEvent
|
||||
case object BITCOIN_FUNDING_SPENT extends BitcoinEvent
|
||||
case object BITCOIN_OUTPUT_SPENT extends BitcoinEvent
|
||||
case class BITCOIN_TX_CONFIRMED(tx: Transaction) extends BitcoinEvent
|
||||
case class BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId: ShortChannelId) extends BitcoinEvent
|
||||
case class BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId: Long) extends BitcoinEvent
|
||||
case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEvent
|
||||
|
||||
/*
|
||||
@ -108,19 +91,17 @@ 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: ByteVector = Sphinx.LAST_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, redirected: Boolean = false) 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_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false) extends Command
|
||||
final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = false) extends Command
|
||||
final case class CMD_FAIL_HTLC(id: Long, reason: Either[BinaryData, FailureMessage], commit: Boolean = false) extends Command
|
||||
final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: BinaryData, 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 object CMD_FORCECLOSE extends Command
|
||||
final case object CMD_GETSTATE extends Command
|
||||
final case object CMD_GETSTATEDATA extends Command
|
||||
final case object CMD_GETINFO extends Command
|
||||
final case class RES_GETINFO(nodeId: PublicKey, channelId: ByteVector32, state: State, data: Data)
|
||||
case object CMD_SIGN extends Command
|
||||
final case class CMD_CLOSE(scriptPubKey: Option[BinaryData]) extends Command
|
||||
case object CMD_GETSTATE extends Command
|
||||
case object CMD_GETSTATEDATA extends Command
|
||||
case object CMD_GETINFO extends Command
|
||||
final case class RES_GETINFO(nodeid: BinaryData, channelId: BinaryData, state: State, data: Data)
|
||||
|
||||
/*
|
||||
8888888b. d8888 88888888888 d8888
|
||||
@ -137,70 +118,63 @@ sealed trait Data
|
||||
|
||||
case object Nothing extends Data
|
||||
|
||||
sealed trait HasCommitments extends Data {
|
||||
trait HasCommitments extends Data {
|
||||
def commitments: Commitments
|
||||
def channelId = commitments.channelId
|
||||
}
|
||||
|
||||
case class ClosingTxProposed(unsignedTx: Transaction, localClosingSigned: ClosingSigned)
|
||||
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTx: List[Transaction], spent: Map[OutPoint, BinaryData])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], spent: Map[OutPoint, BinaryData])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], claimHtlcTimeoutTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], htlcPenaltyTxs: List[Transaction], spent: Map[OutPoint, BinaryData])
|
||||
|
||||
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: Point, 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: Point, 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_CONFIRMED(commitments: Commitments,
|
||||
fundingTx: Option[Transaction],
|
||||
waitingSince: Long,
|
||||
deferred: Option[FundingLocked],
|
||||
lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
|
||||
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends Data with HasCommitments
|
||||
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, lastSent: OpenChannel) extends Data
|
||||
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data
|
||||
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
|
||||
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
|
||||
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, lastSent: FundingLocked) extends Data with HasCommitments
|
||||
final case class DATA_NORMAL(commitments: Commitments,
|
||||
shortChannelId: ShortChannelId,
|
||||
buried: Boolean,
|
||||
channelAnnouncement: Option[ChannelAnnouncement],
|
||||
channelUpdate: ChannelUpdate,
|
||||
shortChannelId: Option[Long],
|
||||
localAnnouncementSignatures: Option[AnnouncementSignatures],
|
||||
localShutdown: Option[Shutdown],
|
||||
remoteShutdown: Option[Shutdown]) extends Data with HasCommitments
|
||||
final case class DATA_SHUTDOWN(commitments: Commitments,
|
||||
localShutdown: Shutdown, remoteShutdown: Shutdown) extends Data with HasCommitments
|
||||
final case class DATA_NEGOTIATING(commitments: Commitments,
|
||||
localShutdown: Shutdown, remoteShutdown: Shutdown,
|
||||
closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection)
|
||||
bestUnpublishedClosingTx_opt: Option[Transaction]) extends Data with HasCommitments {
|
||||
require(!closingTxProposed.isEmpty, "there must always be a list for the current negotiation")
|
||||
require(!commitments.localParams.isFunder || closingTxProposed.forall(!_.isEmpty), "funder must have at least one closing signature for every negotation attempt because it initiates the closing")
|
||||
}
|
||||
localShutdown: Shutdown, remoteShutdown: Shutdown, localClosingSigned: ClosingSigned) extends Data with HasCommitments
|
||||
final case class DATA_CLOSING(commitments: Commitments,
|
||||
mutualCloseProposed: List[Transaction], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have
|
||||
mutualClosePublished: List[Transaction] = Nil,
|
||||
mutualClosePublished: Option[Transaction] = None,
|
||||
localCommitPublished: Option[LocalCommitPublished] = None,
|
||||
remoteCommitPublished: Option[RemoteCommitPublished] = None,
|
||||
nextRemoteCommitPublished: Option[RemoteCommitPublished] = None,
|
||||
futureRemoteCommitPublished: Option[RemoteCommitPublished] = None,
|
||||
revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments {
|
||||
val spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx)
|
||||
require(spendingTxes.size > 0, "there must be at least one tx published in this state")
|
||||
require(mutualClosePublished.isDefined || localCommitPublished.isDefined || remoteCommitPublished.isDefined || nextRemoteCommitPublished.isDefined || revokedCommitPublished.size > 0, "there should be at least one tx published in this state")
|
||||
}
|
||||
|
||||
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,
|
||||
maxAcceptedHtlcs: Int,
|
||||
fundingPrivKey: PrivateKey,
|
||||
revocationSecret: Scalar,
|
||||
paymentKey: Scalar,
|
||||
delayedPaymentKey: Scalar,
|
||||
htlcKey: Scalar,
|
||||
defaultFinalScriptPubKey: BinaryData,
|
||||
shaSeed: BinaryData,
|
||||
isFunder: Boolean,
|
||||
defaultFinalScriptPubKey: ByteVector,
|
||||
globalFeatures: ByteVector,
|
||||
localFeatures: ByteVector)
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData) {
|
||||
// precomputed for performance reasons
|
||||
val paymentBasepoint = paymentKey.toPoint
|
||||
val delayedPaymentBasepoint = delayedPaymentKey.toPoint
|
||||
val revocationBasepoint = revocationSecret.toPoint
|
||||
val htlcBasepoint = htlcKey.toPoint
|
||||
}
|
||||
|
||||
final case class RemoteParams(nodeId: PublicKey,
|
||||
dustLimitSatoshis: Long,
|
||||
@ -214,8 +188,8 @@ final case class RemoteParams(nodeId: PublicKey,
|
||||
paymentBasepoint: Point,
|
||||
delayedPaymentBasepoint: Point,
|
||||
htlcBasepoint: Point,
|
||||
globalFeatures: ByteVector,
|
||||
localFeatures: ByteVector)
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData)
|
||||
|
||||
object ChannelFlags {
|
||||
val AnnounceChannel = 0x01.toByte
|
||||
|
||||
@ -1,33 +1,14 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import akka.event.LoggingAdapter
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi}
|
||||
import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment.Origin
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, UInt64}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
// @formatter:off
|
||||
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
|
||||
@ -35,10 +16,10 @@ case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessag
|
||||
}
|
||||
case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessage], signed: List[UpdateMessage])
|
||||
case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges)
|
||||
case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: ByteVector, remoteSig: ByteVector)
|
||||
case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: BinaryData, remoteSig: BinaryData)
|
||||
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
|
||||
case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs)
|
||||
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: ByteVector32, remotePerCommitmentPoint: Point)
|
||||
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, remotePerCommitmentPoint: Point)
|
||||
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false)
|
||||
// @formatter:on
|
||||
|
||||
@ -58,29 +39,22 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
|
||||
originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, the id of the previous channel
|
||||
remoteNextCommitInfo: Either[WaitingForRevocation, Point],
|
||||
commitInput: InputInfo,
|
||||
remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) {
|
||||
remotePerCommitmentSecrets: ShaChain, channelId: BinaryData) {
|
||||
|
||||
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)
|
||||
def hasTimedoutOutgoingHtlcs(blockheight: Long): Boolean =
|
||||
localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.expiry) ||
|
||||
remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.expiry)
|
||||
|
||||
def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal)
|
||||
|
||||
def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal)
|
||||
|
||||
def announceChannel: Boolean = (channelFlags & 0x01) != 0
|
||||
|
||||
def 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
|
||||
reduced.toRemoteMsat - remoteParams.channelReserveSatoshis * 1000 - feesMsat
|
||||
}
|
||||
}
|
||||
|
||||
object Commitments {
|
||||
object Commitments extends Logging {
|
||||
/**
|
||||
* add a change to our proposed change list
|
||||
*
|
||||
@ -102,16 +76,13 @@ object Commitments {
|
||||
*/
|
||||
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
|
||||
|
||||
val blockCount = Globals.blockCount.get()
|
||||
// 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
|
||||
if (cmd.cltvExpiry < minExpiry) {
|
||||
return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockCount = blockCount))
|
||||
if (cmd.paymentHash.size != 32) {
|
||||
return Left(InvalidPaymentHash(commitments.channelId))
|
||||
}
|
||||
val maxExpiry = blockCount + Channel.MAX_CLTV_EXPIRY
|
||||
// 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))
|
||||
|
||||
val blockCount = Globals.blockCount.get()
|
||||
if (cmd.expiry <= blockCount) {
|
||||
return Left(ExpiryCannotBeInThePast(commitments.channelId, cmd.expiry, blockCount))
|
||||
}
|
||||
|
||||
if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) {
|
||||
@ -119,22 +90,22 @@ object Commitments {
|
||||
}
|
||||
|
||||
// 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.amountMsat, cmd.paymentHash, cmd.expiry, 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
|
||||
val remoteCommit1 = commitments1.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments1.remoteCommit)
|
||||
val reduced = CommitmentSpec.reduce(remoteCommit1.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed)
|
||||
// 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)
|
||||
val htlcValueInFlight = UInt64(reduced.htlcs.map(_.add.amountMsat).sum)
|
||||
if (htlcValueInFlight > commitments1.remoteParams.maxHtlcValueInFlightMsat) {
|
||||
// TODO: this should be a specific UPDATE error
|
||||
return Left(HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.remoteParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight))
|
||||
}
|
||||
|
||||
if (outgoingHtlcs.size > commitments1.remoteParams.maxAcceptedHtlcs) {
|
||||
// the HTLC we are about to create is outgoing, but from their point of view it is incoming
|
||||
val acceptedHtlcs = reduced.htlcs.count(_.direction == IN)
|
||||
if (acceptedHtlcs > commitments1.remoteParams.maxAcceptedHtlcs) {
|
||||
return Left(TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.remoteParams.maxAcceptedHtlcs))
|
||||
}
|
||||
|
||||
@ -154,6 +125,17 @@ object Commitments {
|
||||
throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id)
|
||||
}
|
||||
|
||||
if (add.paymentHash.size != 32) {
|
||||
throw InvalidPaymentHash(commitments.channelId)
|
||||
}
|
||||
|
||||
val blockCount = Globals.blockCount.get()
|
||||
// we need a reasonable amount of time to pull the funds before the sender can get refunded
|
||||
val minExpiry = blockCount + 3
|
||||
if (add.expiry < minExpiry) {
|
||||
throw ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = add.expiry, blockCount = blockCount)
|
||||
}
|
||||
|
||||
if (add.amountMsat < commitments.localParams.htlcMinimumMsat) {
|
||||
throw HtlcValueTooSmall(commitments.channelId, minimum = commitments.localParams.htlcMinimumMsat, actual = add.amountMsat)
|
||||
}
|
||||
@ -161,14 +143,14 @@ object Commitments {
|
||||
// let's compute the current commitment *as seen by us* including this change
|
||||
val commitments1 = addRemoteProposal(commitments, add).copy(remoteNextHtlcId = commitments.remoteNextHtlcId + 1)
|
||||
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)
|
||||
val htlcValueInFlight = UInt64(reduced.htlcs.map(_.add.amountMsat).sum)
|
||||
if (htlcValueInFlight > commitments1.localParams.maxHtlcValueInFlightMsat) {
|
||||
throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)
|
||||
}
|
||||
|
||||
if (incomingHtlcs.size > commitments1.localParams.maxAcceptedHtlcs) {
|
||||
val acceptedHtlcs = reduced.htlcs.count(_.direction == IN)
|
||||
if (acceptedHtlcs > commitments1.localParams.maxAcceptedHtlcs) {
|
||||
throw TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.localParams.maxAcceptedHtlcs)
|
||||
}
|
||||
|
||||
@ -210,9 +192,9 @@ object Commitments {
|
||||
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
}
|
||||
|
||||
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Either[Commitments, (Commitments, Origin, UpdateAddHtlc)] =
|
||||
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Either[Commitments, (Commitments, Origin)] =
|
||||
getHtlcCrossSigned(commitments, OUT, fulfill.id) match {
|
||||
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Right((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id), htlc))
|
||||
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Right((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id)))
|
||||
case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, fulfill.id)
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fulfill.id)
|
||||
}
|
||||
@ -229,17 +211,14 @@ object Commitments {
|
||||
throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
case Some(htlc) =>
|
||||
// we need the shared secret to build the error packet
|
||||
Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match {
|
||||
case Success(sharedSecret) =>
|
||||
val reason = cmd.reason match {
|
||||
case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret)
|
||||
case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure)
|
||||
}
|
||||
val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason)
|
||||
val commitments1 = addLocalProposal(commitments, fail)
|
||||
(commitments1, fail)
|
||||
case Failure(_) => throw new CannotExtractSharedSecret(commitments.channelId, htlc)
|
||||
val sharedSecret = Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).sharedSecret
|
||||
val reason = cmd.reason match {
|
||||
case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret)
|
||||
case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure)
|
||||
}
|
||||
val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason)
|
||||
val commitments1 = addLocalProposal(commitments, fail)
|
||||
(commitments1, fail)
|
||||
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
}
|
||||
|
||||
@ -265,20 +244,20 @@ object Commitments {
|
||||
}
|
||||
}
|
||||
|
||||
def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Either[Commitments, (Commitments, Origin, UpdateAddHtlc)] =
|
||||
def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Either[Commitments, (Commitments, Origin)] =
|
||||
getHtlcCrossSigned(commitments, OUT, fail.id) match {
|
||||
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc))
|
||||
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id)))
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
|
||||
}
|
||||
|
||||
def receiveFailMalformed(commitments: Commitments, fail: UpdateFailMalformedHtlc): Either[Commitments, (Commitments, Origin, UpdateAddHtlc)] = {
|
||||
def receiveFailMalformed(commitments: Commitments, fail: UpdateFailMalformedHtlc): Either[Commitments, (Commitments, Origin)] = {
|
||||
// A receiving node MUST fail the channel if the BADONION bit in failure_code is not set for update_fail_malformed_htlc.
|
||||
if ((fail.failureCode & FailureMessageCodecs.BADONION) == 0) {
|
||||
throw InvalidFailureCode(commitments.channelId)
|
||||
}
|
||||
|
||||
getHtlcCrossSigned(commitments, OUT, fail.id) match {
|
||||
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc))
|
||||
case Some(htlc) => Right((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id)))
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
|
||||
}
|
||||
}
|
||||
@ -289,8 +268,7 @@ object Commitments {
|
||||
}
|
||||
// let's compute the current commitment *as seen by them* with this change taken into account
|
||||
val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw)
|
||||
// update_fee replace each other, so we can remove previous ones
|
||||
val commitments1 = commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee))
|
||||
val commitments1 = addLocalProposal(commitments, fee)
|
||||
val reduced = CommitmentSpec.reduce(commitments1.remoteCommit.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed)
|
||||
|
||||
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
|
||||
@ -309,11 +287,7 @@ object Commitments {
|
||||
throw FundeeCannotSendUpdateFee(commitments.channelId)
|
||||
}
|
||||
|
||||
if (fee.feeratePerKw < fr.acinq.eclair.MinimumFeeratePerKw) {
|
||||
throw FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw)
|
||||
}
|
||||
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) {
|
||||
throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)
|
||||
}
|
||||
@ -324,8 +298,7 @@ object Commitments {
|
||||
// (it also means that we need to check the fee of the initial commitment tx somewhere)
|
||||
|
||||
// let's compute the current commitment *as seen by us* including this change
|
||||
// update_fee replace each other, so we can remove previous ones
|
||||
val commitments1 = commitments.copy(remoteChanges = commitments.remoteChanges.copy(proposed = commitments.remoteChanges.proposed.filterNot(_.isInstanceOf[UpdateFee]) :+ fee))
|
||||
val commitments1 = addRemoteProposal(commitments, fee)
|
||||
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
|
||||
@ -346,11 +319,11 @@ object Commitments {
|
||||
|
||||
def remoteHasChanges(commitments: Commitments): Boolean = commitments.localChanges.acked.size > 0 || commitments.remoteChanges.proposed.size > 0
|
||||
|
||||
def revocationPreimage(seed: ByteVector32, index: Long): ByteVector32 = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index)
|
||||
def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index)
|
||||
|
||||
def revocationHash(seed: ByteVector32, index: Long): ByteVector32 = Crypto.sha256(revocationPreimage(seed, index))
|
||||
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index))
|
||||
|
||||
def sendCommit(commitments: Commitments, keyManager: KeyManager)(implicit log: LoggingAdapter): (Commitments, CommitSig) = {
|
||||
def sendCommit(commitments: Commitments): (Commitments, CommitSig) = {
|
||||
import commitments._
|
||||
commitments.remoteNextCommitInfo match {
|
||||
case Right(_) if !localHasChanges(commitments) =>
|
||||
@ -358,14 +331,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(remoteCommit.index + 1, localParams, remoteParams, commitInput, remoteNextPerCommitmentPoint, spec)
|
||||
val sig = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey)
|
||||
|
||||
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
|
||||
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.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)
|
||||
val htlcKey = Generators.derivePrivKey(localParams.htlcKey, remoteNextPerCommitmentPoint)
|
||||
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, htlcKey))
|
||||
|
||||
// don't sign if they don't get paid
|
||||
val commitSig = CommitSig(
|
||||
@ -384,7 +355,7 @@ object Commitments {
|
||||
}
|
||||
}
|
||||
|
||||
def receiveCommit(commitments: Commitments, commit: CommitSig, keyManager: KeyManager)(implicit log: LoggingAdapter): (Commitments, RevokeAndAck) = {
|
||||
def receiveCommit(commitments: Commitments, commit: CommitSig): (Commitments, RevokeAndAck) = {
|
||||
import commitments._
|
||||
// they sent us a signature for *their* view of *our* next commit tx
|
||||
// so in terms of rev.hashes and indexes we have:
|
||||
@ -403,44 +374,37 @@ 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))
|
||||
|
||||
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)
|
||||
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 1)
|
||||
val (localCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = makeLocalTxs(localCommit.index + 1, localParams, remoteParams, commitInput, localPerCommitmentPoint, spec)
|
||||
val sig = Transactions.sign(localCommitTx, localParams.fundingPrivKey)
|
||||
|
||||
// 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, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature)
|
||||
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
|
||||
throw InvalidCommitmentSignature(commitments.channelId, signedCommitTx.tx)
|
||||
throw InvalidCommitmentSignature(commitments.channelId)
|
||||
}
|
||||
|
||||
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
|
||||
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
|
||||
throw new HtlcSigCountMismatch(commitments.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)
|
||||
}
|
||||
val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(localParams.channelKeyPath), localPerCommitmentPoint))
|
||||
require(commit.htlcSignatures.size == sortedHtlcTxs.size, s"htlc sig count mismatch (received=${commit.htlcSignatures.size}, expected=${sortedHtlcTxs.size})")
|
||||
val localHtlcKey = Generators.derivePrivKey(localParams.htlcKey, localPerCommitmentPoint)
|
||||
val htlcSigs = sortedHtlcTxs.map(Transactions.sign(_, localHtlcKey))
|
||||
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint)
|
||||
// combine the sigs to make signed txes
|
||||
val htlcTxsAndSigs = (sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped.toList.collect {
|
||||
case (htlcTx: HtlcTimeoutTx, localSig, remoteSig) =>
|
||||
if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) {
|
||||
throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx)
|
||||
}
|
||||
require(Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isSuccess, "bad sig")
|
||||
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
|
||||
case (htlcTx: HtlcSuccessTx, localSig, remoteSig) =>
|
||||
// we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig
|
||||
if (Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey) == false) {
|
||||
throw new InvalidHtlcSignature(commitments.channelId, htlcTx.tx)
|
||||
}
|
||||
require(Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey), "bad sig")
|
||||
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
|
||||
}
|
||||
|
||||
// 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 = Generators.perCommitSecret(localParams.shaSeed, commitments.localCommit.index)
|
||||
val localNextPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index + 2)
|
||||
val revocation = RevokeAndAck(
|
||||
channelId = commitments.channelId,
|
||||
perCommitmentSecret = localPerCommitmentSecret,
|
||||
@ -454,70 +418,57 @@ object Commitments {
|
||||
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
|
||||
val ourChanges1 = localChanges.copy(acked = Nil)
|
||||
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
|
||||
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1)
|
||||
// the outgoing following htlcs have been completed (fulfilled or failed) when we received this sig
|
||||
val completedOutgoingHtlcs = commitments.localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add.id) -- localCommit1.spec.htlcs.filter(_.direction == OUT).map(_.add.id)
|
||||
// we remove the newly completed htlcs from the origin map
|
||||
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
|
||||
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1, originChannels = originChannels1)
|
||||
|
||||
logger.debug(s"current commit: index=${localCommit1.index} htlc_in=${localCommit1.spec.htlcs.filter(_.direction == IN).size} htlc_out=${localCommit1.spec.htlcs.filter(_.direction == OUT).size} txid=${localCommit1.publishableTxs.commitTx.tx.txid} tx=${Transaction.write(localCommit1.publishableTxs.commitTx.tx)}")
|
||||
|
||||
(commitments1, revocation)
|
||||
}
|
||||
|
||||
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): (Commitments, Seq[ForwardMessage]) = {
|
||||
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): Commitments = {
|
||||
import commitments._
|
||||
// we receive a revocation because we just sent them a sig for their next commit tx
|
||||
remoteNextCommitInfo match {
|
||||
case Left(_) if revocation.perCommitmentSecret.toPoint != remoteCommit.remotePerCommitmentPoint =>
|
||||
throw InvalidRevocation(commitments.channelId)
|
||||
case Left(WaitingForRevocation(theirNextCommit, _, _, _)) =>
|
||||
val forwards = commitments.remoteChanges.signed collect {
|
||||
// we forward adds downstream only when they have been committed by both sides
|
||||
// it always happen when we receive a revocation, because they send the add, then they sign it, then we sign it
|
||||
case add: UpdateAddHtlc => ForwardAdd(add)
|
||||
// same for fails: we need to make sure that they are in neither commitment before propagating the fail upstream
|
||||
case fail: UpdateFailHtlc =>
|
||||
val origin = commitments.originChannels(fail.id)
|
||||
val add = commitments.remoteCommit.spec.htlcs.find(p => p.direction == IN && p.add.id == fail.id).map(_.add).get
|
||||
ForwardFail(fail, origin, add)
|
||||
// same as above
|
||||
case fail: UpdateFailMalformedHtlc =>
|
||||
val origin = commitments.originChannels(fail.id)
|
||||
val add = commitments.remoteCommit.spec.htlcs.find(p => p.direction == IN && p.add.id == fail.id).map(_.add).get
|
||||
ForwardFailMalformed(fail, origin, add)
|
||||
}
|
||||
// the outgoing following htlcs have been completed (fulfilled or failed) when we received this revocation
|
||||
// they have been removed from both local and remote commitment
|
||||
// (since fulfill/fail are sent by remote, they are (1) signed by them, (2) revoked by us, (3) signed by us, (4) revoked by them
|
||||
val completedOutgoingHtlcs = commitments.remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id) -- theirNextCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id)
|
||||
// we remove the newly completed htlcs from the origin map
|
||||
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
|
||||
val commitments1 = commitments.copy(
|
||||
localChanges = localChanges.copy(signed = Nil, acked = localChanges.acked ++ localChanges.signed),
|
||||
remoteChanges = remoteChanges.copy(signed = Nil),
|
||||
remoteCommit = theirNextCommit,
|
||||
remoteNextCommitInfo = Right(revocation.nextPerCommitmentPoint),
|
||||
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret.toBin, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index),
|
||||
originChannels = originChannels1)
|
||||
(commitments1, forwards)
|
||||
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index))
|
||||
|
||||
commitments1
|
||||
case Right(_) =>
|
||||
throw UnexpectedRevocation(commitments.channelId)
|
||||
}
|
||||
}
|
||||
|
||||
def makeLocalTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, 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(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, localPerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
|
||||
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, localPerCommitmentPoint)
|
||||
val localDelayedPaymentPubkey = Generators.derivePubKey(localParams.delayedPaymentBasepoint, localPerCommitmentPoint)
|
||||
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, 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 commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localParams.paymentBasepoint, 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)
|
||||
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
|
||||
}
|
||||
|
||||
def makeRemoteTxs(keyManager: KeyManager, commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, 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(commitTxNumber: Long, localParams: LocalParams, remoteParams: RemoteParams, commitmentInput: InputInfo, remotePerCommitmentPoint: Point, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = {
|
||||
val localPaymentPubkey = Generators.derivePubKey(localParams.paymentBasepoint, remotePerCommitmentPoint)
|
||||
val localHtlcPubkey = Generators.derivePubKey(localParams.htlcBasepoint, remotePerCommitmentPoint)
|
||||
val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, 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 remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remotePerCommitmentPoint)
|
||||
val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localParams.paymentBasepoint, !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)
|
||||
(commitTx, htlcTimeoutTxs, htlcSuccessTxs)
|
||||
}
|
||||
@ -556,17 +507,17 @@ object Commitments {
|
||||
| toLocal: ${commitments.localCommit.spec.toLocalMsat}
|
||||
| toRemote: ${commitments.localCommit.spec.toRemoteMsat}
|
||||
| htlcs:
|
||||
|${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")}
|
||||
|${commitments.localCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")}
|
||||
|remotecommit:
|
||||
| toLocal: ${commitments.remoteCommit.spec.toLocalMsat}
|
||||
| toRemote: ${commitments.remoteCommit.spec.toRemoteMsat}
|
||||
| htlcs:
|
||||
|${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")}
|
||||
|${commitments.remoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").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")}
|
||||
| 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
|
||||
|${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.expiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,27 +1,9 @@
|
||||
/*
|
||||
* 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.channel
|
||||
|
||||
import akka.actor.Status.Failure
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.channel.Register._
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.Register.{Forward, ForwardFailure, ForwardShortId, ForwardShortIdFailure}
|
||||
|
||||
/**
|
||||
* Created by PM on 26/01/2016.
|
||||
@ -34,34 +16,32 @@ class Register extends Actor with ActorLogging {
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned])
|
||||
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
|
||||
|
||||
override def receive: Receive = main(Map.empty, Map.empty, Map.empty)
|
||||
override def receive: Receive = main(Map.empty, Map.empty)
|
||||
|
||||
def main(channels: Map[ByteVector32, ActorRef], shortIds: Map[ShortChannelId, ByteVector32], channelsTo: Map[ByteVector32, PublicKey]): Receive = {
|
||||
case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId) =>
|
||||
def main(channels: Map[BinaryData, ActorRef], shortIds: Map[Long, BinaryData]): Receive = {
|
||||
case ChannelCreated(channel, _, _, _, temporaryChannelId) =>
|
||||
context.watch(channel)
|
||||
context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId))
|
||||
context become main(channels + (temporaryChannelId -> channel), shortIds)
|
||||
|
||||
case ChannelRestored(channel, _, remoteNodeId, _, channelId, _) =>
|
||||
case ChannelRestored(channel, _, _, _, channelId, _) =>
|
||||
context.watch(channel)
|
||||
context become main(channels + (channelId -> channel), shortIds, channelsTo + (channelId -> remoteNodeId))
|
||||
context become main(channels + (channelId -> channel), shortIds)
|
||||
|
||||
case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) =>
|
||||
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId)
|
||||
case ChannelIdAssigned(channel, temporaryChannelId, channelId) =>
|
||||
context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds)
|
||||
|
||||
case ShortChannelIdAssigned(_, channelId, shortChannelId) =>
|
||||
context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo)
|
||||
case ShortChannelIdAssigned(channel, channelId, shortChannelId) =>
|
||||
context become main(channels, shortIds + (shortChannelId -> channelId))
|
||||
|
||||
case Terminated(actor) if channels.values.toSet.contains(actor) =>
|
||||
val channelId = channels.find(_._2 == actor).get._1
|
||||
val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(ShortChannelId(0L))
|
||||
context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId)
|
||||
val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(0L)
|
||||
context become main(channels - channelId, shortIds - shortChannelId)
|
||||
|
||||
case 'channels => sender ! channels
|
||||
|
||||
case 'shortIds => sender ! shortIds
|
||||
|
||||
case 'channelsTo => sender ! channelsTo
|
||||
|
||||
case fwd@Forward(channelId, msg) =>
|
||||
channels.get(channelId) match {
|
||||
case Some(channel) => channel forward msg
|
||||
@ -79,8 +59,8 @@ class Register extends Actor with ActorLogging {
|
||||
object Register {
|
||||
|
||||
// @formatter:off
|
||||
case class Forward[T](channelId: ByteVector32, message: T)
|
||||
case class ForwardShortId[T](shortChannelId: ShortChannelId, message: T)
|
||||
case class Forward[T](channelId: BinaryData, message: T)
|
||||
case class ForwardShortId[T](shortChannelId: Long, message: T)
|
||||
|
||||
case class ForwardFailure[T](fwd: Forward[T]) extends RuntimeException(s"channel ${fwd.channelId} not found")
|
||||
case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) extends RuntimeException(s"channel ${fwd.shortChannelId} not found")
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import org.spongycastle.util.encoders.Hex
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/**
|
||||
* Bit stream that can be written to and read at both ends (i.e. you can read from the end or the beginning of the stream)
|
||||
*
|
||||
* @param bytes bits packed as bytes, the last byte is padded with 0s
|
||||
* @param offstart offset at which the first bit is in the first byte
|
||||
* @param offend offset at which the last bit is in the last byte
|
||||
*/
|
||||
case class BitStream(bytes: Vector[Byte], offstart: Int, offend: Int) {
|
||||
|
||||
// offstart: 0 1 2 3 4 5 6 7
|
||||
// offend: 7 6 5 4 3 2 1 0
|
||||
import BitStream._
|
||||
|
||||
def bitCount = 8 * bytes.length - offstart - offend
|
||||
|
||||
def isEmpty = bitCount == 0
|
||||
|
||||
/**
|
||||
* append a byte to a bitstream
|
||||
*
|
||||
* @param input byte to append
|
||||
* @return an updated bitstream
|
||||
*/
|
||||
def writeByte(input: Byte): BitStream = offend match {
|
||||
case 0 => this.copy(bytes = this.bytes :+ input)
|
||||
case shift =>
|
||||
val input1 = input & 0xff
|
||||
val last = ((bytes.last | (input1 >>> (8 - shift))) & 0xff).toByte
|
||||
val next = ((input1 << shift) & 0xff).toByte
|
||||
this.copy(bytes = bytes.dropRight(1) ++ Vector(last, next))
|
||||
}
|
||||
|
||||
/**
|
||||
* append bytes to a bitstream
|
||||
*
|
||||
* @param input bytes to append
|
||||
* @return an udpdate bitstream
|
||||
*/
|
||||
def writeBytes(input: Seq[Byte]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeByte(b) }
|
||||
|
||||
/**
|
||||
* append a bit to a bistream
|
||||
*
|
||||
* @param bit bit to append
|
||||
* @return an update bitstream
|
||||
*/
|
||||
def writeBit(bit: Bit): BitStream = offend match {
|
||||
case 0 if bit =>
|
||||
BitStream(bytes :+ 0x80.toByte, offstart, 7)
|
||||
case 0 =>
|
||||
BitStream(bytes :+ 0x00.toByte, offstart, 7)
|
||||
case n if bit =>
|
||||
val last = (bytes.last + (1 << (offend - 1))).toByte
|
||||
BitStream(bytes.updated(bytes.length - 1, last), offstart, offend - 1)
|
||||
case n =>
|
||||
BitStream(bytes, offstart, offend - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* append bits to a bistream
|
||||
*
|
||||
* @param input bits to append
|
||||
* @return an update bitstream
|
||||
*/
|
||||
def writeBits(input: Seq[Bit]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeBit(b) }
|
||||
|
||||
/**
|
||||
* read the last bit from a bitstream
|
||||
*
|
||||
* @return a (stream, bit) pair where stream is an updated bitstream and bit is the last bit
|
||||
*/
|
||||
def popBit: (BitStream, Bit) = offend match {
|
||||
case 7 => BitStream(bytes.dropRight(1), offstart, 0) -> lastBit
|
||||
case n =>
|
||||
val shift = n + 1
|
||||
val last = (bytes.last >>> shift) << shift
|
||||
BitStream(bytes.updated(bytes.length - 1, last.toByte), offstart, offend + 1) -> lastBit
|
||||
}
|
||||
|
||||
/**
|
||||
* read the last byte from a bitstream
|
||||
*
|
||||
* @return a (stream, byte) pair where stream is an updated bitstream and byte is the last byte
|
||||
*/
|
||||
def popByte: (BitStream, Byte) = offend match {
|
||||
case 0 => BitStream(bytes.dropRight(1), offstart, offend) -> bytes.last
|
||||
case shift =>
|
||||
val a = bytes(bytes.length - 2) & 0xff
|
||||
val b = bytes(bytes.length - 1) & 0xff
|
||||
val byte = ((a << (8 - shift)) | (b >>> shift)) & 0xff
|
||||
val a1 = (a >>> shift) << shift
|
||||
BitStream(bytes.dropRight(2) :+ a1.toByte, offstart, offend) -> byte.toByte
|
||||
}
|
||||
|
||||
def popBytes(n: Int): (BitStream, Seq[Byte]) = {
|
||||
@tailrec
|
||||
def loop(stream: BitStream, acc: Seq[Byte]): (BitStream, Seq[Byte]) =
|
||||
if (acc.length == n) (stream, acc) else {
|
||||
val (stream1, value) = stream.popByte
|
||||
loop(stream1, acc :+ value)
|
||||
}
|
||||
|
||||
loop(this, Nil)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the first bit from a bitstream
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def readBit: (BitStream, Bit) = offstart match {
|
||||
case 7 => BitStream(bytes.tail, 0, offend) -> firstBit
|
||||
case _ => BitStream(bytes, offstart + 1, offend) -> firstBit
|
||||
}
|
||||
|
||||
def readBits(count: Int): (BitStream, Seq[Bit]) = {
|
||||
@tailrec
|
||||
def loop(stream: BitStream, acc: Seq[Bit]): (BitStream, Seq[Bit]) = if (acc.length == count) (stream, acc) else {
|
||||
val (stream1, bit) = stream.readBit
|
||||
loop(stream1, acc :+ bit)
|
||||
}
|
||||
|
||||
loop(this, Nil)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the first byte from a bitstream
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def readByte: (BitStream, Byte) = {
|
||||
val byte = ((bytes(0) << offstart) | (bytes(1) >>> (7 - offstart))) & 0xff
|
||||
BitStream(bytes.tail, offstart, offend) -> byte.toByte
|
||||
}
|
||||
|
||||
def isSet(pos: Int): Boolean = {
|
||||
val pos1 = pos + offstart
|
||||
(bytes(pos1 / 8) & (1 << (7 - (pos1 % 8)))) != 0
|
||||
}
|
||||
|
||||
def firstBit = (bytes.head & (1 << (7 - offstart))) != 0
|
||||
|
||||
def lastBit = (bytes.last & (1 << offend)) != 0
|
||||
|
||||
def toBinString: String = "0b" + (for (i <- 0 until bitCount) yield if (isSet(i)) '1' else '0').mkString
|
||||
|
||||
def toHexString: String = "0x" + Hex.toHexString(bytes.toArray).toLowerCase
|
||||
}
|
||||
|
||||
object BitStream {
|
||||
type Bit = Boolean
|
||||
val Zero = false
|
||||
val One = true
|
||||
val empty = BitStream(Vector.empty[Byte], 0, 0)
|
||||
}
|
||||
@ -1,28 +1,11 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.{ByteVector32, Protocol}
|
||||
import fr.acinq.bitcoin.{BinaryData, Protocol}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.spongycastle.crypto.engines.ChaCha7539Engine
|
||||
import org.spongycastle.crypto.engines.{ChaCha7539Engine, ChaChaEngine}
|
||||
import org.spongycastle.crypto.params.{KeyParameter, ParametersWithIV}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/**
|
||||
* Poly1305 authenticator
|
||||
@ -35,13 +18,13 @@ object Poly1305 {
|
||||
* @param data input data
|
||||
* @return a 16 byte authentication tag
|
||||
*/
|
||||
def mac(key: ByteVector, datas: ByteVector*): ByteVector = {
|
||||
def mac(key: BinaryData, data: BinaryData): BinaryData = {
|
||||
val out = new Array[Byte](16)
|
||||
val poly = new org.spongycastle.crypto.macs.Poly1305()
|
||||
poly.init(new KeyParameter(key.toArray))
|
||||
datas.foreach(data => poly.update(data.toArray, 0, data.length.toInt))
|
||||
poly.init(new KeyParameter(key))
|
||||
poly.update(data, 0, data.length)
|
||||
poly.doFinal(out, 0)
|
||||
ByteVector.view(out)
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +34,10 @@ object Poly1305 {
|
||||
*/
|
||||
object ChaCha20 {
|
||||
|
||||
def encrypt(plaintext: ByteVector, key: ByteVector, nonce: ByteVector, counter: Int = 0): ByteVector = {
|
||||
def encrypt(plaintext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
|
||||
val engine = new ChaCha7539Engine()
|
||||
engine.init(true, new ParametersWithIV(new KeyParameter(key.toArray), nonce.toArray))
|
||||
val ciphertext: Array[Byte] = new Array[Byte](plaintext.length.toInt)
|
||||
engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce))
|
||||
val ciphertext: BinaryData = new Array[Byte](plaintext.length)
|
||||
counter match {
|
||||
case 0 => ()
|
||||
case 1 =>
|
||||
@ -63,15 +46,15 @@ object ChaCha20 {
|
||||
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
|
||||
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
|
||||
}
|
||||
val len = engine.processBytes(plaintext.toArray, 0, plaintext.length.toInt, ciphertext, 0)
|
||||
val len = engine.processBytes(plaintext.toArray, 0, plaintext.length, ciphertext, 0)
|
||||
require(len == plaintext.length, "ChaCha20 encryption failed")
|
||||
ByteVector.view(ciphertext)
|
||||
ciphertext
|
||||
}
|
||||
|
||||
def decrypt(ciphertext: ByteVector, key: ByteVector, nonce: ByteVector, counter: Int = 0): ByteVector = {
|
||||
def decrypt(ciphertext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
|
||||
val engine = new ChaCha7539Engine
|
||||
engine.init(false, new ParametersWithIV(new KeyParameter(key.toArray), nonce.toArray))
|
||||
val plaintext: Array[Byte] = new Array[Byte](ciphertext.length.toInt)
|
||||
engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce))
|
||||
val plaintext: BinaryData = new Array[Byte](ciphertext.length)
|
||||
counter match {
|
||||
case 0 => ()
|
||||
case 1 =>
|
||||
@ -80,9 +63,9 @@ object ChaCha20 {
|
||||
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
|
||||
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
|
||||
}
|
||||
val len = engine.processBytes(ciphertext.toArray, 0, ciphertext.length.toInt, plaintext, 0)
|
||||
val len = engine.processBytes(ciphertext.toArray, 0, ciphertext.length, plaintext, 0)
|
||||
require(len == ciphertext.length, "ChaCha20 decryption failed")
|
||||
ByteVector.view(plaintext)
|
||||
plaintext
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +76,6 @@ object ChaCha20 {
|
||||
* This what we should be using (see BOLT #8)
|
||||
*/
|
||||
object ChaCha20Poly1305 extends Logging {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key 32 bytes encryption key
|
||||
@ -102,10 +84,11 @@ object ChaCha20Poly1305 extends Logging {
|
||||
* @param aad additional authentication data. can be empty
|
||||
* @return a (ciphertext, mac) tuple
|
||||
*/
|
||||
def encrypt(key: ByteVector, nonce: ByteVector, plaintext: ByteVector, aad: ByteVector): (ByteVector, ByteVector) = {
|
||||
val polykey = ChaCha20.encrypt(ByteVector32.Zeroes, key, nonce)
|
||||
def encrypt(key: BinaryData, nonce: BinaryData, plaintext: BinaryData, aad: BinaryData): (BinaryData, BinaryData) = {
|
||||
val polykey: BinaryData = ChaCha20.encrypt(new Array[Byte](32), key, nonce)
|
||||
val ciphertext = ChaCha20.encrypt(plaintext, key, nonce, 1)
|
||||
val tag = Poly1305.mac(polykey, aad, pad16(aad), ciphertext, pad16(ciphertext), Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN), Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN))
|
||||
val data = aad ++ pad16(aad) ++ ciphertext ++ pad16(ciphertext) ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
|
||||
val tag = Poly1305.mac(polykey, data)
|
||||
logger.debug(s"encrypt($key, $nonce, $aad, $plaintext) = ($ciphertext, $tag)")
|
||||
(ciphertext, tag)
|
||||
}
|
||||
@ -119,19 +102,80 @@ object ChaCha20Poly1305 extends Logging {
|
||||
* @param mac authentication mac
|
||||
* @return the decrypted plaintext if the mac is valid.
|
||||
*/
|
||||
def decrypt(key: ByteVector, nonce: ByteVector, ciphertext: ByteVector, aad: ByteVector, mac: ByteVector): ByteVector = {
|
||||
val polykey = ChaCha20.encrypt(ByteVector32.Zeroes, key, nonce)
|
||||
val tag = Poly1305.mac(polykey, aad, pad16(aad), ciphertext, pad16(ciphertext), Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN), Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN))
|
||||
def decrypt(key: BinaryData, nonce: BinaryData, ciphertext: BinaryData, aad: BinaryData, mac: BinaryData): BinaryData = {
|
||||
val polykey: BinaryData = ChaCha20.encrypt(new Array[Byte](32), key, nonce)
|
||||
val data = aad ++ pad16(aad) ++ ciphertext ++ pad16(ciphertext) ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
|
||||
val tag = Poly1305.mac(polykey, data)
|
||||
require(tag == mac, "invalid mac")
|
||||
val plaintext = ChaCha20.decrypt(ciphertext, key, nonce, 1)
|
||||
logger.debug(s"decrypt($key, $nonce, $aad, $ciphertext, $mac) = $plaintext")
|
||||
plaintext
|
||||
}
|
||||
|
||||
def pad16(data: ByteVector): ByteVector =
|
||||
def pad16(data: Seq[Byte]): Seq[Byte] =
|
||||
if (data.size % 16 == 0)
|
||||
ByteVector.empty
|
||||
Seq.empty[Byte]
|
||||
else
|
||||
ByteVector.fill(16 - (data.size % 16))(0)
|
||||
Seq.fill[Byte](16 - (data.size % 16))(0)
|
||||
}
|
||||
|
||||
object ChaCha20Legacy {
|
||||
def encrypt(plaintext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
|
||||
val engine = new ChaChaEngine(20)
|
||||
engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce))
|
||||
val ciphertext: BinaryData = new Array[Byte](plaintext.length)
|
||||
counter match {
|
||||
case 0 => ()
|
||||
case 1 =>
|
||||
// skip 1 block == set counter to 1 instead of 0
|
||||
val dummy = new Array[Byte](64)
|
||||
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
|
||||
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
|
||||
}
|
||||
val len = engine.processBytes(plaintext.toArray, 0, plaintext.length, ciphertext, 0)
|
||||
require(len == plaintext.length, "ChaCha20Legacy encryption failed")
|
||||
ciphertext
|
||||
}
|
||||
|
||||
def decrypt(ciphertext: BinaryData, key: BinaryData, nonce: BinaryData, counter: Int = 0): BinaryData = {
|
||||
val engine = new ChaChaEngine(20)
|
||||
engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce))
|
||||
val plaintext: BinaryData = new Array[Byte](ciphertext.length)
|
||||
counter match {
|
||||
case 0 => ()
|
||||
case 1 =>
|
||||
// skip 1 block == set counter to 1 instead of 0
|
||||
val dummy = new Array[Byte](64)
|
||||
engine.processBytes(new Array[Byte](64), 0, 64, dummy, 0)
|
||||
case _ => throw new RuntimeException(s"chacha20 counter must be 0 or 1")
|
||||
}
|
||||
val len = engine.processBytes(ciphertext.toArray, 0, ciphertext.length, plaintext, 0)
|
||||
require(len == ciphertext.length, "ChaCha20Legacy decryption failed")
|
||||
plaintext
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy implementation of ChaCha20Poly1305
|
||||
* Nonce is 8 bytes instead of 12 and the output tag computation is different
|
||||
*
|
||||
* Used in our first interop tests with lightning-c, should not be needed anymore
|
||||
*/
|
||||
object Chacha20Poly1305Legacy {
|
||||
def encrypt(key: BinaryData, nonce: BinaryData, plaintext: BinaryData, aad: BinaryData): (BinaryData, BinaryData) = {
|
||||
val polykey: BinaryData = ChaCha20Legacy.encrypt(new Array[Byte](32), key, nonce)
|
||||
val ciphertext = ChaCha20Legacy.encrypt(plaintext, key, nonce, 1)
|
||||
val data = aad ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ ciphertext ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
|
||||
val tag = Poly1305.mac(polykey, data)
|
||||
(ciphertext, tag)
|
||||
}
|
||||
|
||||
def decrypt(key: BinaryData, nonce: BinaryData, ciphertext: BinaryData, aad: BinaryData, mac: BinaryData): BinaryData = {
|
||||
val polykey: BinaryData = ChaCha20Legacy.encrypt(new Array[Byte](32), key, nonce)
|
||||
val data = aad ++ Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN) ++ ciphertext ++ Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)
|
||||
val tag = Poly1305.mac(polykey, data)
|
||||
require(tag == mac, "invalid mac")
|
||||
val plaintext = ChaCha20Legacy.decrypt(ciphertext, key, nonce, 1)
|
||||
plaintext
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +1,21 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
|
||||
/**
|
||||
* Created by PM on 07/12/2016.
|
||||
*/
|
||||
object Generators {
|
||||
|
||||
def fixSize(data: ByteVector): ByteVector32 = data.length match {
|
||||
case 32 => ByteVector32(data)
|
||||
case length if length < 32 => ByteVector32(data.padLeft(32))
|
||||
def fixSize(data: BinaryData): BinaryData = data.length match {
|
||||
case 32 => data
|
||||
case length if length < 32 => Array.fill(32 - length)(0.toByte) ++ data
|
||||
}
|
||||
|
||||
def perCommitSecret(seed: ByteVector32, index: Long): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFL - index))
|
||||
def perCommitSecret(seed: BinaryData, index: Long): Scalar = Scalar(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFL - index))
|
||||
|
||||
def perCommitPoint(seed: ByteVector32, index: Long): Point = perCommitSecret(seed, index).toPoint
|
||||
def perCommitPoint(seed: BinaryData, index: Long): Point = perCommitSecret(seed, index).toPoint
|
||||
|
||||
def derivePrivKey(secret: Scalar, perCommitPoint: Point): PrivateKey = {
|
||||
// secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint)
|
||||
|
||||
@ -1,77 +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.crypto
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto, DeterministicWallet}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
trait KeyManager {
|
||||
def nodeKey: DeterministicWallet.ExtendedPrivateKey
|
||||
|
||||
def nodeId: PublicKey
|
||||
|
||||
def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
|
||||
|
||||
def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
|
||||
|
||||
def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
|
||||
|
||||
def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
|
||||
|
||||
def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPublicKey
|
||||
|
||||
def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.Scalar
|
||||
|
||||
def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.Point
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @return a signature generated with the private key that matches the input
|
||||
* extended public key
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector
|
||||
|
||||
/**
|
||||
* This method is used to spend funds send to htlc keys/delayed keys
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @param remotePoint remote point
|
||||
* @return a signature generated with a private key generated from the input keys's matching
|
||||
* private key and the remote point.
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): ByteVector
|
||||
|
||||
/**
|
||||
* Ths method is used to spend revoked transactions, with the corresponding revocation key
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @param remoteSecret remote secret
|
||||
* @return a signature generated with a private key generated from the input keys's matching
|
||||
* private key and the remote secret.
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): ByteVector
|
||||
|
||||
def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector, ByteVector)
|
||||
}
|
||||
@ -1,149 +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.crypto
|
||||
|
||||
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _}
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, 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 scodec.bits.ByteVector
|
||||
|
||||
object LocalKeyManager {
|
||||
def channelKeyBasePath(chainHash: ByteVector32) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil
|
||||
case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil
|
||||
}
|
||||
|
||||
|
||||
// WARNING: if you change this path, you will change your node id even if the seed remains the same!!!
|
||||
// Note that the node path and the above channel path are on different branches so even if the
|
||||
// node key is compromised there is no way to retrieve the wallet keys
|
||||
def nodeKeyBasePath(chainHash: ByteVector32) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil
|
||||
case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(0) :: Nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class manages secrets and private keys.
|
||||
* It exports points and public keys, and provides signing methods
|
||||
*
|
||||
* @param seed seed from which keys will be derived
|
||||
*/
|
||||
class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyManager {
|
||||
private val master = DeterministicWallet.generate(seed)
|
||||
|
||||
override val nodeKey = DeterministicWallet.derivePrivateKey(master, LocalKeyManager.nodeKeyBasePath(chainHash))
|
||||
override val nodeId = nodeKey.publicKey
|
||||
|
||||
private val privateKeys: LoadingCache[KeyPath, ExtendedPrivateKey] = CacheBuilder.newBuilder()
|
||||
.maximumSize(6 * 200) // 6 keys per channel * 200 channels
|
||||
.build[KeyPath, ExtendedPrivateKey](new CacheLoader[KeyPath, ExtendedPrivateKey] {
|
||||
override def load(keyPath: KeyPath): ExtendedPrivateKey = derivePrivateKey(master, keyPath)
|
||||
})
|
||||
|
||||
private val publicKeys: LoadingCache[KeyPath, ExtendedPublicKey] = CacheBuilder.newBuilder()
|
||||
.maximumSize(6 * 200) // 6 keys per channel * 200 channels
|
||||
.build[KeyPath, ExtendedPublicKey](new CacheLoader[KeyPath, ExtendedPublicKey] {
|
||||
override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath))
|
||||
})
|
||||
|
||||
private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalKeyManager.channelKeyBasePath(chainHash) ++ channelKeyPath.path) :+ index
|
||||
|
||||
private def fundingPrivateKey(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(0)))
|
||||
|
||||
private def revocationSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(1)))
|
||||
|
||||
private def paymentSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(2)))
|
||||
|
||||
private def delayedPaymentSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(3)))
|
||||
|
||||
private def htlcSecret(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(4)))
|
||||
|
||||
private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath) = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.toBin)
|
||||
|
||||
override def fundingPublicKey(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(0)))
|
||||
|
||||
override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(1)))
|
||||
|
||||
override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(2)))
|
||||
|
||||
override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(3)))
|
||||
|
||||
override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath) = publicKeys.get(internalKeyPath(channelKeyPath, hardened(4)))
|
||||
|
||||
override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long) = Generators.perCommitSecret(shaSeed(channelKeyPath), index)
|
||||
|
||||
override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long) = Generators.perCommitPoint(shaSeed(channelKeyPath), index)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @return a signature generated with the private key that matches the input
|
||||
* extended public key
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey): ByteVector = {
|
||||
val privateKey = privateKeys.get(publicKey.path)
|
||||
Transactions.sign(tx, privateKey.privateKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to spend funds send to htlc keys/delayed keys
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @param remotePoint remote point
|
||||
* @return a signature generated with a private key generated from the input keys's matching
|
||||
* private key and the remote point.
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: Point): ByteVector = {
|
||||
val privateKey = privateKeys.get(publicKey.path)
|
||||
val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint)
|
||||
Transactions.sign(tx, currentKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ths method is used to spend revoked transactions, with the corresponding revocation key
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @param publicKey extended public key
|
||||
* @param remoteSecret remote secret
|
||||
* @return a signature generated with a private key generated from the input keys's matching
|
||||
* private key and the remote secret.
|
||||
*/
|
||||
def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: Scalar): ByteVector = {
|
||||
val privateKey = privateKeys.get(publicKey.path)
|
||||
val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret)
|
||||
Transactions.sign(tx, currentKey)
|
||||
}
|
||||
|
||||
override def signChannelAnnouncement(channelKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector, ByteVector) = {
|
||||
val witness = if (Announcements.isNode1(nodeId, remoteNodeId)) {
|
||||
Announcements.channelAnnouncementWitnessEncode(chainHash, shortChannelId, nodeId, remoteNodeId, fundingPublicKey(channelKeyPath).publicKey, remoteFundingKey, features)
|
||||
} else {
|
||||
Announcements.channelAnnouncementWitnessEncode(chainHash, shortChannelId, remoteNodeId, nodeId, remoteFundingKey, fundingPublicKey(channelKeyPath).publicKey, features)
|
||||
}
|
||||
val nodeSig = Crypto.encodeSignature(Crypto.sign(witness, nodeKey.privateKey)) :+ 1.toByte
|
||||
val bitcoinSig = Crypto.encodeSignature(Crypto.sign(witness, fundingPrivateKey(channelKeyPath).privateKey)) :+ 1.toByte
|
||||
(nodeSig, bitcoinSig)
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,21 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.{Crypto, Protocol}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Protocol}
|
||||
import fr.acinq.eclair.randomBytes
|
||||
import grizzled.slf4j.Logging
|
||||
import org.spongycastle.crypto.digests.SHA256Digest
|
||||
import org.spongycastle.crypto.macs.HMac
|
||||
import org.spongycastle.crypto.params.KeyParameter
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/**
|
||||
* see http://noiseprotocol.org/
|
||||
*/
|
||||
object Noise {
|
||||
|
||||
case class KeyPair(pub: ByteVector, priv: ByteVector)
|
||||
case class KeyPair(pub: BinaryData, priv: BinaryData)
|
||||
|
||||
/**
|
||||
* Diffie-Helmann functions
|
||||
@ -40,9 +23,9 @@ object Noise {
|
||||
trait DHFunctions {
|
||||
def name: String
|
||||
|
||||
def generateKeyPair(priv: ByteVector): KeyPair
|
||||
def generateKeyPair(priv: BinaryData): KeyPair
|
||||
|
||||
def dh(keyPair: KeyPair, publicKey: ByteVector): ByteVector
|
||||
def dh(keyPair: KeyPair, publicKey: BinaryData): BinaryData
|
||||
|
||||
def dhLen: Int
|
||||
|
||||
@ -52,7 +35,7 @@ object Noise {
|
||||
object Secp256k1DHFunctions extends DHFunctions {
|
||||
override val name = "secp256k1"
|
||||
|
||||
override def generateKeyPair(priv: ByteVector): KeyPair = {
|
||||
override def generateKeyPair(priv: BinaryData): KeyPair = {
|
||||
require(priv.length == 32)
|
||||
KeyPair(Crypto.publicKeyFromPrivateKey(priv :+ 1.toByte), priv)
|
||||
}
|
||||
@ -64,11 +47,11 @@ object Noise {
|
||||
* @param publicKey
|
||||
* @return sha256(publicKey * keyPair.priv in compressed format)
|
||||
*/
|
||||
override def dh(keyPair: KeyPair, publicKey: ByteVector): ByteVector = {
|
||||
val point = Crypto.curve.getCurve.decodePoint(publicKey.toArray)
|
||||
override def dh(keyPair: KeyPair, publicKey: BinaryData): BinaryData = {
|
||||
val point = Crypto.curve.getCurve.decodePoint(publicKey)
|
||||
val scalar = new BigInteger(1, keyPair.priv.take(32).toArray)
|
||||
val point1 = point.multiply(scalar).normalize()
|
||||
Crypto.sha256(ByteVector.view(point1.getEncoded(true)))
|
||||
Crypto.sha256(point1.getEncoded(true))
|
||||
}
|
||||
|
||||
override def dhLen: Int = 32
|
||||
@ -86,29 +69,29 @@ object Noise {
|
||||
// for the key k. Returns the ciphertext. Encryption must be done with an "AEAD" encryption mode with the associated
|
||||
// data ad (using the terminology from [1]) and returns a ciphertext that is the same size as the plaintext
|
||||
// plus 16 bytes for authentication data. The entire ciphertext must be indistinguishable from random if the key is secret.
|
||||
def encrypt(k: ByteVector, n: Long, ad: ByteVector, plaintext: ByteVector): ByteVector
|
||||
def encrypt(k: BinaryData, n: Long, ad: BinaryData, plaintext: BinaryData): BinaryData
|
||||
|
||||
// Decrypts ciphertext using a cipher key k of 32 bytes, an 8-byte unsigned integer nonce n, and associated data ad.
|
||||
// Returns the plaintext, unless authentication fails, in which case an error is signaled to the caller.
|
||||
def decrypt(k: ByteVector, n: Long, ad: ByteVector, ciphertext: ByteVector): ByteVector
|
||||
def decrypt(k: BinaryData, n: Long, ad: BinaryData, ciphertext: BinaryData): BinaryData
|
||||
}
|
||||
|
||||
object Chacha20Poly1305CipherFunctions extends CipherFunctions {
|
||||
override val name = "ChaChaPoly"
|
||||
|
||||
// as specified in BOLT #8
|
||||
def nonce(n: Long): ByteVector = ByteVector.fill(4)(0) ++ Protocol.writeUInt64(n, ByteOrder.LITTLE_ENDIAN)
|
||||
def nonce(n: Long): BinaryData = BinaryData("00000000") ++ Protocol.writeUInt64(n, ByteOrder.LITTLE_ENDIAN)
|
||||
|
||||
//Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique
|
||||
override def encrypt(k: ByteVector, n: Long, ad: ByteVector, plaintext: ByteVector): ByteVector = {
|
||||
override def encrypt(k: BinaryData, n: Long, ad: BinaryData, plaintext: BinaryData): BinaryData = {
|
||||
val (ciphertext, mac) = ChaCha20Poly1305.encrypt(k, nonce(n), plaintext, ad)
|
||||
ciphertext ++ mac
|
||||
}
|
||||
|
||||
// Decrypts ciphertext using a cipher key k of 32 bytes, an 8-byte unsigned integer nonce n, and associated data ad.
|
||||
override def decrypt(k: ByteVector, n: Long, ad: ByteVector, ciphertextAndMac: ByteVector): ByteVector = {
|
||||
val ciphertext: ByteVector = ciphertextAndMac.dropRight(16)
|
||||
val mac: ByteVector = ciphertextAndMac.takeRight(16)
|
||||
override def decrypt(k: BinaryData, n: Long, ad: BinaryData, ciphertextAndMac: BinaryData): BinaryData = {
|
||||
val ciphertext: BinaryData = ciphertextAndMac.dropRight(16)
|
||||
val mac: BinaryData = ciphertextAndMac.takeRight(16)
|
||||
ChaCha20Poly1305.decrypt(k, nonce(n), ciphertext, ad, mac)
|
||||
}
|
||||
}
|
||||
@ -120,7 +103,7 @@ object Noise {
|
||||
def name: String
|
||||
|
||||
// Hashes some arbitrary-length data with a collision-resistant cryptographic hash function and returns an output of HASHLEN bytes.
|
||||
def hash(data: ByteVector): ByteVector
|
||||
def hash(data: BinaryData): BinaryData
|
||||
|
||||
// A constant specifying the size in bytes of the hash output. Must be 32 or 64.
|
||||
def hashLen: Int
|
||||
@ -129,17 +112,17 @@ object Noise {
|
||||
def blockLen: Int
|
||||
|
||||
// Applies HMAC from [2] using the HASH() function. This function is only called as part of HKDF(), below.
|
||||
def hmacHash(key: ByteVector, data: ByteVector): ByteVector
|
||||
def hmacHash(key: BinaryData, data: BinaryData): BinaryData
|
||||
|
||||
// Takes a chaining_key byte sequence of length HASHLEN, and an input_key_material byte sequence with length either zero bytes, 32 bytes, or DHLEN bytes. Returns two byte sequences of length HASHLEN, as follows:
|
||||
// Sets temp_key = HMAC-HASH(chaining_key, input_key_material).
|
||||
// Sets output1 = HMAC-HASH(temp_key, byte(0x01)).
|
||||
// Sets output2 = HMAC-HASH(temp_key, output1 || byte(0x02)).
|
||||
// Returns the pair (output1, output2).
|
||||
def hkdf(chainingKey: ByteVector, inputMaterial: ByteVector): (ByteVector, ByteVector) = {
|
||||
def hkdf(chainingKey: BinaryData, inputMaterial: BinaryData): (BinaryData, BinaryData) = {
|
||||
val tempkey = hmacHash(chainingKey, inputMaterial)
|
||||
val output1 = hmacHash(tempkey, ByteVector(0x01))
|
||||
val output2 = hmacHash(tempkey, output1 ++ ByteVector(0x02))
|
||||
val output1 = hmacHash(tempkey, Seq(0x01.toByte))
|
||||
val output2 = hmacHash(tempkey, output1 ++ Seq(0x02.toByte))
|
||||
logger.debug(s"HKDF($chainingKey, $inputMaterial) = ($output1, $output2)")
|
||||
|
||||
(output1, output2)
|
||||
@ -153,15 +136,15 @@ object Noise {
|
||||
|
||||
override val blockLen = 64
|
||||
|
||||
override def hash(data: ByteVector) = Crypto.sha256(data)
|
||||
override def hash(data: BinaryData) = Crypto.sha256(data)
|
||||
|
||||
override def hmacHash(key: ByteVector, data: ByteVector): ByteVector = {
|
||||
override def hmacHash(key: BinaryData, data: BinaryData) = {
|
||||
val mac = new HMac(new SHA256Digest())
|
||||
mac.init(new KeyParameter(key.toArray))
|
||||
mac.update(data.toArray, 0, data.length.toInt)
|
||||
mac.update(data.toArray, 0, data.length)
|
||||
val out = new Array[Byte](32)
|
||||
mac.doFinal(out, 0)
|
||||
ByteVector.view(out)
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,13 +154,13 @@ object Noise {
|
||||
trait CipherState {
|
||||
def cipher: CipherFunctions
|
||||
|
||||
def initializeKey(key: ByteVector): CipherState = CipherState(key, cipher)
|
||||
def initializeKey(key: BinaryData): CipherState = CipherState(key, cipher)
|
||||
|
||||
def hasKey: Boolean
|
||||
|
||||
def encryptWithAd(ad: ByteVector, plaintext: ByteVector): (CipherState, ByteVector)
|
||||
def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData)
|
||||
|
||||
def decryptWithAd(ad: ByteVector, ciphertext: ByteVector): (CipherState, ByteVector)
|
||||
def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,12 +168,12 @@ object Noise {
|
||||
*
|
||||
* @param cipher cipher functions
|
||||
*/
|
||||
case class UninitializedCipherState(cipher: CipherFunctions) extends CipherState {
|
||||
case class UnitializedCipherState(cipher: CipherFunctions) extends CipherState {
|
||||
override val hasKey = false
|
||||
|
||||
override def encryptWithAd(ad: ByteVector, plaintext: ByteVector): (CipherState, ByteVector) = (this, plaintext)
|
||||
override def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = (this, plaintext)
|
||||
|
||||
override def decryptWithAd(ad: ByteVector, ciphertext: ByteVector): (CipherState, ByteVector) = (this, ciphertext)
|
||||
override def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = (this, ciphertext)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,25 +183,25 @@ object Noise {
|
||||
* @param n nonce
|
||||
* @param cipher cipher functions
|
||||
*/
|
||||
case class InitializedCipherState(k: ByteVector, n: Long, cipher: CipherFunctions) extends CipherState {
|
||||
case class InitializedCipherState(k: BinaryData, n: Long, cipher: CipherFunctions) extends CipherState {
|
||||
require(k.length == 32)
|
||||
|
||||
def hasKey = true
|
||||
|
||||
def encryptWithAd(ad: ByteVector, plaintext: ByteVector): (CipherState, ByteVector) = {
|
||||
def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = {
|
||||
(this.copy(n = this.n + 1), cipher.encrypt(k, n, ad, plaintext))
|
||||
}
|
||||
|
||||
def decryptWithAd(ad: ByteVector, ciphertext: ByteVector): (CipherState, ByteVector) = (this.copy(n = this.n + 1), cipher.decrypt(k, n, ad, ciphertext))
|
||||
def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = (this.copy(n = this.n + 1), cipher.decrypt(k, n, ad, ciphertext))
|
||||
}
|
||||
|
||||
object CipherState {
|
||||
def apply(k: ByteVector, cipher: CipherFunctions): CipherState = k.length match {
|
||||
case 0 => UninitializedCipherState(cipher)
|
||||
def apply(k: BinaryData, cipher: CipherFunctions): CipherState = k.length match {
|
||||
case 0 => UnitializedCipherState(cipher)
|
||||
case 32 => InitializedCipherState(k, 0, cipher)
|
||||
}
|
||||
|
||||
def apply(cipher: CipherFunctions): CipherState = UninitializedCipherState(cipher)
|
||||
def apply(cipher: CipherFunctions): CipherState = UnitializedCipherState(cipher)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -228,41 +211,41 @@ object Noise {
|
||||
* @param h hash
|
||||
* @param hashFunctions hash functions
|
||||
*/
|
||||
case class SymmetricState(cipherState: CipherState, ck: ByteVector, h: ByteVector, hashFunctions: HashFunctions) extends Logging {
|
||||
def mixKey(inputKeyMaterial: ByteVector): SymmetricState = {
|
||||
case class SymmetricState(cipherState: CipherState, ck: BinaryData, h: BinaryData, hashFunctions: HashFunctions) extends Logging {
|
||||
def mixKey(inputKeyMaterial: BinaryData): SymmetricState = {
|
||||
logger.debug(s"ss = 0x$inputKeyMaterial")
|
||||
val (ck1, tempk) = hashFunctions.hkdf(ck, inputKeyMaterial)
|
||||
val tempk1: ByteVector = hashFunctions.hashLen match {
|
||||
val tempk1: BinaryData = hashFunctions.hashLen match {
|
||||
case 32 => tempk
|
||||
case 64 => tempk.take(32)
|
||||
}
|
||||
this.copy(cipherState = cipherState.initializeKey(tempk1), ck = ck1)
|
||||
}
|
||||
|
||||
def mixHash(data: ByteVector): SymmetricState = {
|
||||
def mixHash(data: BinaryData): SymmetricState = {
|
||||
this.copy(h = hashFunctions.hash(h ++ data))
|
||||
}
|
||||
|
||||
def encryptAndHash(plaintext: ByteVector): (SymmetricState, ByteVector) = {
|
||||
def encryptAndHash(plaintext: BinaryData): (SymmetricState, BinaryData) = {
|
||||
val (cipherstate1, ciphertext) = cipherState.encryptWithAd(h, plaintext)
|
||||
(this.copy(cipherState = cipherstate1).mixHash(ciphertext), ciphertext)
|
||||
}
|
||||
|
||||
def decryptAndHash(ciphertext: ByteVector): (SymmetricState, ByteVector) = {
|
||||
def decryptAndHash(ciphertext: BinaryData): (SymmetricState, BinaryData) = {
|
||||
val (cipherstate1, plaintext) = cipherState.decryptWithAd(h, ciphertext)
|
||||
(this.copy(cipherState = cipherstate1).mixHash(ciphertext), plaintext)
|
||||
}
|
||||
|
||||
def split: (CipherState, CipherState, ByteVector) = {
|
||||
val (tempk1, tempk2) = hashFunctions.hkdf(ck, ByteVector.empty)
|
||||
def split: (CipherState, CipherState, BinaryData) = {
|
||||
val (tempk1, tempk2) = hashFunctions.hkdf(ck, BinaryData.empty)
|
||||
(cipherState.initializeKey(tempk1.take(32)), cipherState.initializeKey(tempk2.take(32)), ck)
|
||||
}
|
||||
}
|
||||
|
||||
object SymmetricState {
|
||||
def apply(protocolName: ByteVector, cipherFunctions: CipherFunctions, hashFunctions: HashFunctions): SymmetricState = {
|
||||
val h: ByteVector = if (protocolName.length <= hashFunctions.hashLen)
|
||||
protocolName ++ ByteVector.fill(hashFunctions.hashLen - protocolName.length)(0)
|
||||
def apply(protocolName: BinaryData, cipherFunctions: CipherFunctions, hashFunctions: HashFunctions): SymmetricState = {
|
||||
val h: BinaryData = if (protocolName.length <= hashFunctions.hashLen)
|
||||
protocolName ++ Seq.fill[Byte](hashFunctions.hashLen - protocolName.length)(0)
|
||||
else hashFunctions.hash(protocolName)
|
||||
|
||||
new SymmetricState(CipherState(cipherFunctions), ck = h, h = h, hashFunctions)
|
||||
@ -307,7 +290,7 @@ object Noise {
|
||||
val handshakePatternXK = HandshakePattern("XK", initiatorPreMessages = Nil, responderPreMessages = S :: Nil, messages = List(E :: ES :: Nil, E :: EE :: Nil, S :: SE :: Nil))
|
||||
|
||||
trait ByteStream {
|
||||
def nextBytes(length: Int): ByteVector
|
||||
def nextBytes(length: Int): BinaryData
|
||||
}
|
||||
|
||||
object RandomBytes extends ByteStream {
|
||||
@ -317,7 +300,7 @@ object Noise {
|
||||
|
||||
sealed trait HandshakeState
|
||||
|
||||
case class HandshakeStateWriter(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
|
||||
case class HandshakeStateWriter(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
|
||||
def toReader: HandshakeStateReader = HandshakeStateReader(messages, state, s, e, rs, re, dh, byteStream)
|
||||
|
||||
/**
|
||||
@ -328,11 +311,11 @@ object Noise {
|
||||
* When the handshake is over (i.e. there are no more handshake patterns to process) the last item will
|
||||
* contain 2 cipherstates than can be used to encrypt/decrypt further communication
|
||||
*/
|
||||
def write(payload: ByteVector): (HandshakeStateReader, ByteVector, Option[(CipherState, CipherState, ByteVector)]) = {
|
||||
def write(payload: BinaryData): (HandshakeStateReader, BinaryData, Option[(CipherState, CipherState, BinaryData)]) = {
|
||||
require(!messages.isEmpty)
|
||||
logger.debug(s"write($payload)")
|
||||
|
||||
val (writer1, buffer1) = messages.head.foldLeft(this -> ByteVector.empty) {
|
||||
val (writer1, buffer1) = messages.head.foldLeft(this -> BinaryData.empty) {
|
||||
case ((writer, buffer), pattern) => pattern match {
|
||||
case E =>
|
||||
val e1 = dh.generateKeyPair(byteStream.nextBytes(dh.dhLen))
|
||||
@ -360,17 +343,17 @@ object Noise {
|
||||
val buffer2 = buffer1 ++ ciphertext
|
||||
val writer2 = writer1.copy(messages = messages.tail, state = state1)
|
||||
logger.debug(s"h = 0x${state1.h}")
|
||||
logger.debug(s"output: 0x$buffer2")
|
||||
logger.debug(s"output: 0x${BinaryData(buffer2)}")
|
||||
|
||||
(writer2.toReader, buffer2, if (messages.tail.isEmpty) Some(writer2.state.split) else None)
|
||||
}
|
||||
}
|
||||
|
||||
object HandshakeStateWriter {
|
||||
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions): HandshakeStateWriter = new HandshakeStateWriter(messages, state, s, e, rs, re, dh, RandomBytes)
|
||||
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions): HandshakeStateWriter = new HandshakeStateWriter(messages, state, s, e, rs, re, dh, RandomBytes)
|
||||
}
|
||||
|
||||
case class HandshakeStateReader(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
|
||||
case class HandshakeStateReader(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, byteStream: ByteStream) extends HandshakeState with Logging {
|
||||
def toWriter: HandshakeStateWriter = HandshakeStateWriter(messages, state, s, e, rs, re, dh, byteStream)
|
||||
|
||||
/** *
|
||||
@ -381,7 +364,7 @@ object Noise {
|
||||
* next message. When the handshake is over (i.e. there are no more handshake patterns to process) the last item will
|
||||
* contain 2 cipherstates than can be used to encrypt/decrypt further communication
|
||||
*/
|
||||
def read(message: ByteVector): (HandshakeStateWriter, ByteVector, Option[(CipherState, CipherState, ByteVector)]) = {
|
||||
def read(message: BinaryData): (HandshakeStateWriter, BinaryData, Option[(CipherState, CipherState, BinaryData)]) = {
|
||||
logger.debug(s"input: 0x$message")
|
||||
val (reader1, buffer1) = messages.head.foldLeft(this -> message) {
|
||||
case ((reader, buffer), pattern) => pattern match {
|
||||
@ -420,18 +403,18 @@ object Noise {
|
||||
}
|
||||
|
||||
object HandshakeStateReader {
|
||||
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions): HandshakeStateReader = new HandshakeStateReader(messages, state, s, e, rs, re, dh, RandomBytes)
|
||||
def apply(messages: List[MessagePatterns], state: SymmetricState, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions): HandshakeStateReader = new HandshakeStateReader(messages, state, s, e, rs, re, dh, RandomBytes)
|
||||
}
|
||||
|
||||
object HandshakeState {
|
||||
|
||||
private def makeSymmetricState(handshakePattern: HandshakePattern, prologue: ByteVector, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions): SymmetricState = {
|
||||
private def makeSymmetricState(handshakePattern: HandshakePattern, prologue: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions): SymmetricState = {
|
||||
val name = "Noise_" + handshakePattern.name + "_" + dh.name + "_" + cipher.name + "_" + hash.name
|
||||
val symmetricState = SymmetricState(ByteVector.view(name.getBytes("UTF-8")), cipher, hash)
|
||||
val symmetricState = SymmetricState(name.getBytes("UTF-8"), cipher, hash)
|
||||
symmetricState.mixHash(prologue)
|
||||
}
|
||||
|
||||
def initializeWriter(handshakePattern: HandshakePattern, prologue: ByteVector, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateWriter = {
|
||||
def initializeWriter(handshakePattern: HandshakePattern, prologue: BinaryData, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateWriter = {
|
||||
val symmetricState = makeSymmetricState(handshakePattern, prologue, dh, cipher, hash)
|
||||
val symmetricState1 = (handshakePattern.initiatorPreMessages).foldLeft(symmetricState) {
|
||||
case (state, E) => state.mixHash(e.pub)
|
||||
@ -446,7 +429,7 @@ object Noise {
|
||||
HandshakeStateWriter(handshakePattern.messages, symmetricState2, s, e, rs, re, dh, byteStream)
|
||||
}
|
||||
|
||||
def initializeReader(handshakePattern: HandshakePattern, prologue: ByteVector, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateReader = {
|
||||
def initializeReader(handshakePattern: HandshakePattern, prologue: BinaryData, s: KeyPair, e: KeyPair, rs: BinaryData, re: BinaryData, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateReader = {
|
||||
val symmetricState = makeSymmetricState(handshakePattern, prologue, dh, cipher, hash)
|
||||
val symmetricState1 = handshakePattern.initiatorPreMessages.foldLeft(symmetricState) {
|
||||
case (state, E) => state.mixHash(re)
|
||||
|
||||
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import fr.acinq.bitcoin._
|
||||
@ -27,9 +11,11 @@ import scala.annotation.tailrec
|
||||
*/
|
||||
object ShaChain {
|
||||
|
||||
case class Node(value: ByteVector32, height: Int, parent: Option[Node])
|
||||
case class Node(value: BinaryData, height: Int, parent: Option[Node]) {
|
||||
require(value.length == 32)
|
||||
}
|
||||
|
||||
def flip(in: ByteVector32, index: Int): ByteVector32 = ByteVector32(in.update(index / 8, (in(index / 8) ^ (1 << index % 8)).toByte))
|
||||
def flip(in: BinaryData, index: Int): BinaryData = in.data.updated(index / 8, (in.data(index / 8) ^ (1 << index % 8)).toByte)
|
||||
|
||||
/**
|
||||
*
|
||||
@ -53,16 +39,16 @@ object ShaChain {
|
||||
|
||||
def derive(node: Node, directions: Long): Node = derive(node, moves(directions))
|
||||
|
||||
def shaChainFromSeed(hash: ByteVector32, index: Long) = derive(Node(hash, 0, None), index).value
|
||||
def shaChainFromSeed(hash: BinaryData, index: Long) = derive(Node(hash, 0, None), index).value
|
||||
|
||||
type Index = Vector[Boolean]
|
||||
|
||||
val empty = ShaChain(Map.empty[Index, ByteVector32])
|
||||
val empty = ShaChain(Map.empty[Index, BinaryData])
|
||||
|
||||
val init = empty
|
||||
|
||||
@tailrec
|
||||
def addHash(receiver: ShaChain, hash: ByteVector32, index: Index): ShaChain = {
|
||||
def addHash(receiver: ShaChain, hash: BinaryData, index: Index): ShaChain = {
|
||||
index.last match {
|
||||
case true => ShaChain(receiver.knownHashes + (index -> hash))
|
||||
case false =>
|
||||
@ -75,19 +61,19 @@ object ShaChain {
|
||||
}
|
||||
}
|
||||
|
||||
def addHash(receiver: ShaChain, hash: ByteVector32, index: Long): ShaChain = {
|
||||
def addHash(receiver: ShaChain, hash: BinaryData, index: Long): ShaChain = {
|
||||
receiver.lastIndex.map(value => require(index == value - 1L))
|
||||
addHash(receiver, hash, moves(index)).copy(lastIndex = Some(index))
|
||||
}
|
||||
|
||||
def getHash(receiver: ShaChain, index: Index): Option[ByteVector32] = {
|
||||
def getHash(receiver: ShaChain, index: Index): Option[BinaryData] = {
|
||||
receiver.knownHashes.keys.find(key => index.startsWith(key)).map(key => {
|
||||
val root = Node(receiver.knownHashes(key), key.length, None)
|
||||
derive(root, index.drop(key.length)).value
|
||||
})
|
||||
}
|
||||
|
||||
def getHash(receiver: ShaChain, index: Long): Option[ByteVector32] = {
|
||||
def getHash(receiver: ShaChain, index: Long): Option[BinaryData] = {
|
||||
receiver.lastIndex match {
|
||||
case None => None
|
||||
case Some(value) if value > index => None
|
||||
@ -95,14 +81,14 @@ object ShaChain {
|
||||
}
|
||||
}
|
||||
|
||||
def iterator(chain: ShaChain): Iterator[ByteVector32] = chain.lastIndex match {
|
||||
def iterator(chain: ShaChain): Iterator[BinaryData] = chain.lastIndex match {
|
||||
case None => Iterator.empty
|
||||
case Some(index) => new Iterator[ByteVector32] {
|
||||
case Some(index) => new Iterator[BinaryData] {
|
||||
var pos = index
|
||||
|
||||
override def hasNext: Boolean = pos >= index && pos <= 0xffffffffffffffffL
|
||||
|
||||
override def next(): ByteVector32 = {
|
||||
override def next(): BinaryData = {
|
||||
val value = chain.getHash(pos).get
|
||||
pos = pos + 1
|
||||
value
|
||||
@ -116,12 +102,12 @@ object ShaChain {
|
||||
import scodec.bits.BitVector
|
||||
import scodec.codecs._
|
||||
|
||||
// codec for a single map entry (i.e. Vector[Boolean] -> ByteVector
|
||||
val entryCodec = vectorOfN(uint16, bool) ~ variableSizeBytes(uint16, LightningMessageCodecs.bytes32)
|
||||
// codec for a single map entry (i.e. Vector[Boolean] -> BinaryData
|
||||
val entryCodec = vectorOfN(uint16, bool) ~ LightningMessageCodecs.varsizebinarydata
|
||||
|
||||
// codec for a Map[Vector[Boolean], ByteVector]: write all k -> v pairs using the codec defined above
|
||||
val mapCodec: Codec[Map[Vector[Boolean], ByteVector32]] = Codec[Map[Vector[Boolean], ByteVector32]](
|
||||
(m: Map[Vector[Boolean], ByteVector32]) => vectorOfN(uint16, entryCodec).encode(m.toVector),
|
||||
// codec for a Map[Vector[Boolean], BinaryData]: write all k ->v pairs using the codec defined above
|
||||
val mapCodec: Codec[Map[Vector[Boolean], BinaryData]] = Codec[Map[Vector[Boolean], BinaryData]](
|
||||
(m: Map[Vector[Boolean], BinaryData]) => vectorOfN(uint16, entryCodec).encode(m.toVector),
|
||||
(b: BitVector) => vectorOfN(uint16, entryCodec).decode(b).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
@ -138,8 +124,8 @@ object ShaChain {
|
||||
* @param lastIndex index of the last known hash. Hashes are supposed to be added in reverse order i.e.
|
||||
* from 0xFFFFFFFFFFFFFFFF down to 0
|
||||
*/
|
||||
case class ShaChain(knownHashes: Map[Vector[Boolean], ByteVector32], lastIndex: Option[Long] = None) {
|
||||
def addHash(hash: ByteVector32, index: Long): ShaChain = ShaChain.addHash(this, hash, index)
|
||||
case class ShaChain(knownHashes: Map[Vector[Boolean], BinaryData], lastIndex: Option[Long] = None) {
|
||||
def addHash(hash: BinaryData, index: Long): ShaChain = ShaChain.addHash(this, hash, index)
|
||||
|
||||
def getHash(index: Long) = ShaChain.getHash(this, index)
|
||||
|
||||
|
||||
@ -1,35 +1,18 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream}
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto, Protocol}
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Protocol}
|
||||
import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.spongycastle.crypto.digests.SHA256Digest
|
||||
import org.spongycastle.crypto.macs.HMac
|
||||
import org.spongycastle.crypto.params.KeyParameter
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.bits.BitVector
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by fabrice on 13/01/17.
|
||||
@ -51,78 +34,83 @@ object Sphinx extends Logging {
|
||||
val PacketLength = 1 + 33 + MacLength + MaxHops * (PayloadLength + MacLength)
|
||||
|
||||
// last packet (all zeroes except for the version byte)
|
||||
val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(MaxHops * (PayloadLength + MacLength))(0))
|
||||
val LAST_PACKET = Packet(Version, zeroes(33), zeroes(MacLength), zeroes(MaxHops * (PayloadLength + MacLength)))
|
||||
|
||||
def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = {
|
||||
def hmac256(key: Seq[Byte], message: Seq[Byte]): Seq[Byte] = {
|
||||
val mac = new HMac(new SHA256Digest())
|
||||
mac.init(new KeyParameter(key.toArray))
|
||||
mac.update(message.toArray, 0, message.length.toInt)
|
||||
mac.update(message.toArray, 0, message.length)
|
||||
val output = new Array[Byte](32)
|
||||
mac.doFinal(output, 0)
|
||||
ByteVector32(ByteVector.view(output))
|
||||
output
|
||||
}
|
||||
|
||||
def mac(key: ByteVector, message: ByteVector): ByteVector32 = hmac256(key, message)
|
||||
def mac(key: BinaryData, message: BinaryData): BinaryData = hmac256(key, message).take(MacLength)
|
||||
|
||||
def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = hmac256(keyType, secret)
|
||||
def xor(a: Seq[Byte], b: Seq[Byte]): Seq[Byte] = a.zip(b).map { case (x, y) => ((x ^ y) & 0xff).toByte }
|
||||
|
||||
def generateKey(keyType: String, secret: ByteVector32): ByteVector32 = generateKey(ByteVector.view(keyType.getBytes("UTF-8")), secret)
|
||||
def generateKey(keyType: BinaryData, secret: BinaryData): BinaryData = {
|
||||
require(secret.length == 32, "secret must be 32 bytes")
|
||||
hmac256(keyType, secret)
|
||||
}
|
||||
|
||||
def zeroes(length: Int): ByteVector = ByteVector.fill(length)(0)
|
||||
def generateKey(keyType: String, secret: BinaryData): BinaryData = generateKey(keyType.getBytes("UTF-8"), secret)
|
||||
|
||||
def generateStream(key: ByteVector, length: Int): ByteVector = ChaCha20.encrypt(zeroes(length), key, zeroes(12))
|
||||
def zeroes(length: Int): BinaryData = Seq.fill[Byte](length)(0)
|
||||
|
||||
def computeSharedSecret(pub: PublicKey, secret: PrivateKey): ByteVector32 = Crypto.sha256(ByteVector.view(pub.multiply(secret).normalize().getEncoded(true)))
|
||||
def generateStream(key: BinaryData, length: Int): BinaryData = ChaCha20Legacy.encrypt(zeroes(length), key, zeroes(8))
|
||||
|
||||
def computeblindingFactor(pub: PublicKey, secret: ByteVector): ByteVector32 = Crypto.sha256(pub.toBin ++ secret)
|
||||
def computeSharedSecret(pub: PublicKey, secret: PrivateKey): BinaryData = Crypto.sha256(pub.multiply(secret).normalize().getEncoded(true))
|
||||
|
||||
def blind(pub: PublicKey, blindingFactor: ByteVector32): PublicKey = PublicKey(pub.multiply(Scalar(blindingFactor)).normalize(), compressed = true)
|
||||
def computeblindingFactor(pub: PublicKey, secret: BinaryData): BinaryData = Crypto.sha256(pub.toBin ++ secret)
|
||||
|
||||
def blind(pub: PublicKey, blindingFactors: Seq[ByteVector32]): PublicKey = blindingFactors.foldLeft(pub)(blind)
|
||||
def blind(pub: PublicKey, blindingFactor: BinaryData): PublicKey = PublicKey(pub.multiply(blindingFactor).normalize(), compressed = true)
|
||||
|
||||
def blind(pub: PublicKey, blindingFactors: Seq[BinaryData]): PublicKey = blindingFactors.foldLeft(pub)(blind)
|
||||
|
||||
/**
|
||||
* computes the ephemeral public keys and shared secrets for all nodes on the route.
|
||||
* computes the ephemereal public keys and shared secrets for all nodes on the route.
|
||||
*
|
||||
* @param sessionKey this node's session key
|
||||
* @param publicKeys public keys of each node on the route
|
||||
* @return a tuple (ephemeral public keys, shared secrets)
|
||||
* @return a tuple (ephemereal public keys, shared secrets)
|
||||
*/
|
||||
def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = {
|
||||
val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG, compressed = true), sessionKey.value.toBin)
|
||||
val secret0 = computeSharedSecret(publicKeys.head, sessionKey)
|
||||
val blindingFactor0 = computeblindingFactor(ephemeralPublicKey0, secret0)
|
||||
computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemeralPublicKey0), Seq(blindingFactor0), Seq(secret0))
|
||||
def computeEphemerealPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[BinaryData]) = {
|
||||
val ephemerealPublicKey0 = blind(PublicKey(Crypto.curve.getG, compressed = true), sessionKey.value)
|
||||
val secret0 = computeSharedSecret(publicKeys(0), sessionKey)
|
||||
val blindingFactor0 = computeblindingFactor(ephemerealPublicKey0, secret0)
|
||||
computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemerealPublicKey0), Seq(blindingFactor0), Seq(secret0))
|
||||
}
|
||||
|
||||
@tailrec
|
||||
def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemeralPublicKeys: Seq[PublicKey], blindingFactors: Seq[ByteVector32], sharedSecrets: Seq[ByteVector32]): (Seq[PublicKey], Seq[ByteVector32]) = {
|
||||
def computeEphemerealPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemerealPublicKeys: Seq[PublicKey], blindingFactors: Seq[BinaryData], sharedSecrets: Seq[BinaryData]): (Seq[PublicKey], Seq[BinaryData]) = {
|
||||
if (publicKeys.isEmpty)
|
||||
(ephemeralPublicKeys, sharedSecrets)
|
||||
(ephemerealPublicKeys, sharedSecrets)
|
||||
else {
|
||||
val ephemeralPublicKey = blind(ephemeralPublicKeys.last, blindingFactors.last)
|
||||
val ephemerealPublicKey = blind(ephemerealPublicKeys.last, blindingFactors.last)
|
||||
val secret = computeSharedSecret(blind(publicKeys.head, blindingFactors), sessionKey)
|
||||
val blindingFactor = computeblindingFactor(ephemeralPublicKey, secret)
|
||||
computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, ephemeralPublicKeys :+ ephemeralPublicKey, blindingFactors :+ blindingFactor, sharedSecrets :+ secret)
|
||||
val blindingFactor = computeblindingFactor(ephemerealPublicKey, secret)
|
||||
computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, ephemerealPublicKeys :+ ephemerealPublicKey, blindingFactors :+ blindingFactor, sharedSecrets :+ secret)
|
||||
}
|
||||
}
|
||||
|
||||
def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], hopSize: Int, maxNumberOfHops: Int = MaxHops): ByteVector = {
|
||||
sharedSecrets.foldLeft(ByteVector.empty)((padding, secret) => {
|
||||
def generateFiller(keyType: String, sharedSecrets: Seq[BinaryData], hopSize: Int, maxNumberOfHops: Int = MaxHops): BinaryData = {
|
||||
sharedSecrets.foldLeft(Seq.empty[Byte])((padding, secret) => {
|
||||
val key = generateKey(keyType, secret)
|
||||
val padding1 = padding ++ ByteVector.fill(hopSize)(0)
|
||||
val padding1 = padding ++ zeroes(hopSize)
|
||||
val stream = generateStream(key, hopSize * (maxNumberOfHops + 1)).takeRight(padding1.length)
|
||||
padding1.xor(stream)
|
||||
xor(padding1, stream)
|
||||
})
|
||||
}
|
||||
|
||||
case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, routingInfo: ByteVector) {
|
||||
case class Packet(version: Int, publicKey: BinaryData, hmac: BinaryData, routingInfo: BinaryData) {
|
||||
require(publicKey.length == 33, "onion packet public key length should be 33")
|
||||
require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength")
|
||||
require(routingInfo.length == MaxHops * (PayloadLength + MacLength), s"onion packet routing info length should be ${MaxHops * (PayloadLength + MacLength)}")
|
||||
|
||||
def isLastPacket: Boolean = hmac == ByteVector32.Zeroes
|
||||
def isLastPacket: Boolean = hmac == zeroes(MacLength)
|
||||
|
||||
def serialize: ByteVector = Packet.write(this)
|
||||
def serialize: BinaryData = Packet.write(this)
|
||||
}
|
||||
|
||||
object Packet {
|
||||
@ -134,26 +122,26 @@ object Sphinx extends Logging {
|
||||
in.read(routingInfo)
|
||||
val hmac = new Array[Byte](MacLength)
|
||||
in.read(hmac)
|
||||
Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(routingInfo))
|
||||
Packet(version, publicKey, hmac, routingInfo)
|
||||
}
|
||||
|
||||
def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray))
|
||||
def read(in: BinaryData): Packet = read(new ByteArrayInputStream(in))
|
||||
|
||||
def write(packet: Packet, out: OutputStream): OutputStream = {
|
||||
out.write(packet.version)
|
||||
out.write(packet.publicKey.toArray)
|
||||
out.write(packet.routingInfo.toArray)
|
||||
out.write(packet.hmac.toArray)
|
||||
out.write(packet.publicKey)
|
||||
out.write(packet.routingInfo)
|
||||
out.write(packet.hmac)
|
||||
out
|
||||
}
|
||||
|
||||
def write(packet: Packet): ByteVector = {
|
||||
def write(packet: Packet): BinaryData = {
|
||||
val out = new ByteArrayOutputStream(PacketLength)
|
||||
write(packet, out)
|
||||
ByteVector.view(out.toByteArray)
|
||||
out.toByteArray
|
||||
}
|
||||
|
||||
def isLastPacket(packet: ByteVector): Boolean = Packet.read(packet).hmac == ByteVector32.Zeroes
|
||||
def isLastPacket(packet: BinaryData): Boolean = Packet.read(packet).hmac == zeroes(MacLength)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,7 +150,7 @@ object Sphinx extends Logging {
|
||||
* @param nextPacket packet for the next node
|
||||
* @param sharedSecret shared secret for the sending node, which we will need to return error messages
|
||||
*/
|
||||
case class ParsedPacket(payload: ByteVector, nextPacket: Packet, sharedSecret: ByteVector32)
|
||||
case class ParsedPacket(payload: BinaryData, nextPacket: Packet, sharedSecret: BinaryData)
|
||||
|
||||
/**
|
||||
*
|
||||
@ -175,31 +163,30 @@ object Sphinx extends Logging {
|
||||
* - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure
|
||||
* messages upstream.
|
||||
*/
|
||||
def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try {
|
||||
def parsePacket(privateKey: PrivateKey, associatedData: BinaryData, rawPacket: BinaryData): ParsedPacket = {
|
||||
require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be ${PacketLength}")
|
||||
val packet = Packet.read(rawPacket)
|
||||
val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey)
|
||||
val mu = generateKey("mu", sharedSecret)
|
||||
val check = mac(mu, packet.routingInfo ++ associatedData)
|
||||
val check: BinaryData = mac(mu, packet.routingInfo ++ associatedData)
|
||||
require(check == packet.hmac, "invalid header mac")
|
||||
|
||||
val rho = generateKey("rho", sharedSecret)
|
||||
val bin = (packet.routingInfo ++ ByteVector.fill(PayloadLength + MacLength)(0)) xor generateStream(rho, PayloadLength + MacLength + MaxHops * (PayloadLength + MacLength))
|
||||
val bin = xor(packet.routingInfo ++ zeroes(PayloadLength + MacLength), generateStream(rho, PayloadLength + MacLength + MaxHops * (PayloadLength + MacLength)))
|
||||
val payload = bin.take(PayloadLength)
|
||||
val hmac = ByteVector32(bin.slice(PayloadLength, PayloadLength + MacLength))
|
||||
val nextRouteInfo = bin.drop(PayloadLength + MacLength)
|
||||
val hmac = bin.slice(PayloadLength, PayloadLength + MacLength)
|
||||
val nextRoutinfo = bin.drop(PayloadLength + MacLength)
|
||||
|
||||
val nextPubKey = blind(PublicKey(packet.publicKey), computeblindingFactor(PublicKey(packet.publicKey), sharedSecret))
|
||||
|
||||
ParsedPacket(payload, Packet(Version, nextPubKey, hmac, nextRouteInfo), sharedSecret)
|
||||
ParsedPacket(payload, Packet(Version, nextPubKey, hmac, nextRoutinfo), sharedSecret)
|
||||
}
|
||||
|
||||
@tailrec
|
||||
private def extractSharedSecrets(packet: ByteVector, privateKey: PrivateKey, associatedData: ByteVector32, acc: Seq[ByteVector32] = Nil): Try[Seq[ByteVector32]] = {
|
||||
def extractSharedSecrets(packet: BinaryData, privateKey: PrivateKey, associatedData: BinaryData, acc: Seq[BinaryData] = Nil): Seq[BinaryData] = {
|
||||
parsePacket(privateKey, associatedData, packet) match {
|
||||
case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret)
|
||||
case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret)
|
||||
case Failure(t) => Failure(t)
|
||||
case ParsedPacket(_, nextPacket, sharedSecret) if nextPacket.isLastPacket => acc :+ sharedSecret
|
||||
case ParsedPacket(_, nextPacket, sharedSecret) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret)
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,23 +199,23 @@ object Sphinx extends Logging {
|
||||
*
|
||||
* @param payload payload for this packed
|
||||
* @param associatedData associated data
|
||||
* @param ephemeralPublicKey ephemeral key for this packed
|
||||
* @param ephemerealPublicKey ephemereal key for this packed
|
||||
* @param sharedSecret shared secret
|
||||
* @param packet current packet (1 + all zeroes if this is the last packet)
|
||||
* @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet
|
||||
* @return the next packet
|
||||
*/
|
||||
private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, routingInfoFiller: ByteVector = ByteVector.empty): Packet = {
|
||||
def makeNextPacket(payload: BinaryData, associatedData: BinaryData, ephemerealPublicKey: BinaryData, sharedSecret: BinaryData, packet: Packet, routingInfoFiller: BinaryData = BinaryData.empty): Packet = {
|
||||
require(payload.length == PayloadLength)
|
||||
|
||||
val nextRoutingInfo = {
|
||||
val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(PayloadLength + MacLength)
|
||||
val routingInfo2 = routingInfo1 xor generateStream(generateKey("rho", sharedSecret), MaxHops * (PayloadLength + MacLength))
|
||||
val routingInfo2 = xor(routingInfo1, generateStream(generateKey("rho", sharedSecret), MaxHops * (PayloadLength + MacLength)))
|
||||
routingInfo2.dropRight(routingInfoFiller.length) ++ routingInfoFiller
|
||||
}
|
||||
|
||||
val nextHmac = mac(generateKey("mu", sharedSecret), nextRoutingInfo ++ associatedData)
|
||||
val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextRoutingInfo)
|
||||
val nextHmac: BinaryData = mac(generateKey("mu", sharedSecret), nextRoutingInfo ++ associatedData)
|
||||
val nextPacket = Packet(Version, ephemerealPublicKey, nextHmac, nextRoutingInfo)
|
||||
nextPacket
|
||||
}
|
||||
|
||||
@ -239,7 +226,7 @@ object Sphinx extends Logging {
|
||||
* @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the
|
||||
* packet. Empty if you're just forwarding the packet to the next node
|
||||
*/
|
||||
case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(ByteVector32, PublicKey)])
|
||||
case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(BinaryData, PublicKey)])
|
||||
|
||||
/**
|
||||
* A properly decoded error from a node in the route
|
||||
@ -259,21 +246,21 @@ object Sphinx extends Logging {
|
||||
* @return an OnionPacket(onion packet, shared secrets). the onion packet can be sent to the first node in the list, and the
|
||||
* shared secrets (one per node) can be used to parse returned error messages if needed
|
||||
*/
|
||||
def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = {
|
||||
val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys)
|
||||
def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[BinaryData], associatedData: BinaryData): PacketAndSecrets = {
|
||||
val (ephemerealPublicKeys, sharedsecrets) = computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys)
|
||||
val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, MaxHops)
|
||||
|
||||
val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last, sharedsecrets.last, LAST_PACKET, filler)
|
||||
val lastPacket = makeNextPacket(payloads.last, associatedData, ephemerealPublicKeys.last, sharedsecrets.last, LAST_PACKET, filler)
|
||||
|
||||
@tailrec
|
||||
def loop(hoppayloads: Seq[ByteVector], ephkeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = {
|
||||
def loop(hoppayloads: Seq[BinaryData], ephkeys: Seq[PublicKey], sharedSecrets: Seq[BinaryData], packet: Packet): Packet = {
|
||||
if (hoppayloads.isEmpty) packet else {
|
||||
val nextPacket = makeNextPacket(hoppayloads.last, associatedData, ephkeys.last, sharedSecrets.last, packet)
|
||||
loop(hoppayloads.dropRight(1), ephkeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket)
|
||||
}
|
||||
}
|
||||
|
||||
val packet = loop(payloads.dropRight(1), ephemeralPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket)
|
||||
val packet = loop(payloads.dropRight(1), ephemerealPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket)
|
||||
PacketAndSecrets(packet, sharedsecrets.zip(publicKeys))
|
||||
}
|
||||
|
||||
@ -294,15 +281,15 @@ object Sphinx extends Logging {
|
||||
* @param failure failure message
|
||||
* @return an error packet that can be sent to the destination node
|
||||
*/
|
||||
def createErrorPacket(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = {
|
||||
val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector
|
||||
def createErrorPacket(sharedSecret: BinaryData, failure: FailureMessage): BinaryData = {
|
||||
val message: BinaryData = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteArray
|
||||
require(message.length <= MaxErrorPayloadLength, s"error message length is ${message.length}, it must be less than $MaxErrorPayloadLength")
|
||||
val um = Sphinx.generateKey("um", sharedSecret)
|
||||
val padlen = MaxErrorPayloadLength - message.length
|
||||
val payload = Protocol.writeUInt16(message.length.toInt, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padlen.toInt, ByteOrder.BIG_ENDIAN) ++ ByteVector.fill(padlen.toInt)(0)
|
||||
val payload = Protocol.writeUInt16(message.length, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padlen, ByteOrder.BIG_ENDIAN) ++ Sphinx.zeroes(padlen)
|
||||
logger.debug(s"um key: $um")
|
||||
logger.debug(s"error payload: ${payload.toHex}")
|
||||
logger.debug(s"raw error packet: ${(Sphinx.mac(um, payload) ++ payload).toHex}")
|
||||
logger.debug(s"error payload: ${BinaryData(payload)}")
|
||||
logger.debug(s"raw error packet: ${BinaryData(Sphinx.mac(um, payload) ++ payload)}")
|
||||
forwardErrorPacket(Sphinx.mac(um, payload) ++ payload, sharedSecret)
|
||||
}
|
||||
|
||||
@ -311,10 +298,10 @@ object Sphinx extends Logging {
|
||||
* @param packet error packet
|
||||
* @return the failure message that is embedded in the error packet
|
||||
*/
|
||||
private def extractFailureMessage(packet: ByteVector): FailureMessage = {
|
||||
def extractFailureMessage(packet: BinaryData): FailureMessage = {
|
||||
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
|
||||
val (mac, payload) = packet.splitAt(Sphinx.MacLength)
|
||||
val len = Protocol.uint16(payload.toArray, ByteOrder.BIG_ENDIAN)
|
||||
val len = Protocol.uint16(payload, ByteOrder.BIG_ENDIAN)
|
||||
require((len >= 0) && (len <= MaxErrorPayloadLength), s"message length must be less than $MaxErrorPayloadLength")
|
||||
FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value
|
||||
}
|
||||
@ -325,13 +312,13 @@ object Sphinx extends Logging {
|
||||
* @param sharedSecret destination node's shared secret
|
||||
* @return an obfuscated error packet that can be sent to the destination node
|
||||
*/
|
||||
def forwardErrorPacket(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = {
|
||||
def forwardErrorPacket(packet: BinaryData, sharedSecret: BinaryData): BinaryData = {
|
||||
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
|
||||
val key = generateKey("ammag", sharedSecret)
|
||||
val stream = generateStream(key, ErrorPacketLength)
|
||||
logger.debug(s"ammag key: $key")
|
||||
logger.debug(s"error stream: $stream")
|
||||
packet xor stream
|
||||
Sphinx.xor(packet, stream)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -340,10 +327,10 @@ object Sphinx extends Logging {
|
||||
* @param packet error packet
|
||||
* @return true if the packet's mac is valid, which means that it has been properly de-obfuscated
|
||||
*/
|
||||
private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = {
|
||||
def checkMac(sharedSecret: BinaryData, packet: BinaryData): Boolean = {
|
||||
val (mac, payload) = packet.splitAt(Sphinx.MacLength)
|
||||
val um = Sphinx.generateKey("um", sharedSecret)
|
||||
ByteVector32(mac) == Sphinx.mac(um, payload)
|
||||
BinaryData(mac) == Sphinx.mac(um, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -352,20 +339,17 @@ object Sphinx extends Logging {
|
||||
*
|
||||
* @param packet error packet
|
||||
* @param sharedSecrets nodes shared secrets
|
||||
* @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise
|
||||
* @return Some(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, none otherwise
|
||||
*/
|
||||
def parseErrorPacket(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try {
|
||||
@tailrec
|
||||
def parseErrorPacket(packet: BinaryData, sharedSecrets: Seq[(BinaryData, PublicKey)]): Option[ErrorPacket] = {
|
||||
require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength")
|
||||
|
||||
@tailrec
|
||||
def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match {
|
||||
case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets")
|
||||
sharedSecrets match {
|
||||
case Nil => None
|
||||
case (secret, pubkey) :: tail =>
|
||||
val packet1 = forwardErrorPacket(packet, secret)
|
||||
if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail)
|
||||
if (checkMac(secret, packet1)) Some(ErrorPacket(pubkey, extractFailureMessage(packet1))) else parseErrorPacket(packet1, tail)
|
||||
}
|
||||
|
||||
loop(packet, sharedSecrets)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,40 +1,19 @@
|
||||
/*
|
||||
* 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.crypto
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ExtendedActorSystem, FSM, PoisonPill, Props, Terminated}
|
||||
import akka.event.Logging.MDC
|
||||
import akka.event._
|
||||
import akka.io.Tcp
|
||||
import akka.actor.{Actor, ActorRef, FSM, Props, Terminated}
|
||||
import akka.io.Tcp.{PeerClosed, _}
|
||||
import akka.util.ByteString
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Protocol
|
||||
import fr.acinq.bitcoin.{BinaryData, Protocol}
|
||||
import fr.acinq.eclair.crypto.Noise._
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement, _}
|
||||
import fr.acinq.eclair.{Diagnostics, FSMDiagnosticActorLogging, Logs}
|
||||
import scodec.bits.ByteVector
|
||||
import fr.acinq.eclair.io.WriteAckSender
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec, DecodeResult}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable.Queue
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* see BOLT #8
|
||||
@ -49,38 +28,15 @@ import scala.util.{Failure, Success, Try}
|
||||
* @param rs remote node static public key (which must be known before we initiate communication)
|
||||
* @param connection actor that represents the other node's
|
||||
*/
|
||||
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[T]) extends Actor with FSMDiagnosticActorLogging[TransportHandler.State, TransportHandler.Data] {
|
||||
|
||||
// will hold the peer's public key once it is available (we don't know it right away in case of an incoming connection)
|
||||
var remoteNodeId_opt: Option[PublicKey] = rs.map(PublicKey(_))
|
||||
|
||||
val wireLog = new BusLogging(context.system.eventStream, "", classOf[Diagnostics], context.system.asInstanceOf[ExtendedActorSystem].logFilter) with DiagnosticLoggingAdapter
|
||||
|
||||
def diag(message: T, direction: String) = {
|
||||
require(direction == "IN" || direction == "OUT")
|
||||
val channelId_opt = message match {
|
||||
case msg: HasTemporaryChannelId => Some(msg.temporaryChannelId)
|
||||
case msg: HasChannelId => Some(msg.channelId)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
wireLog.mdc(Logs.mdc(remoteNodeId_opt, channelId_opt))
|
||||
if (channelId_opt.isDefined) {
|
||||
// channel-related messages are logged as info
|
||||
wireLog.info(s"$direction msg={}", message)
|
||||
} else {
|
||||
// other messages (e.g. routing gossip) are logged as debug
|
||||
wireLog.debug(s"$direction msg={}", message)
|
||||
}
|
||||
wireLog.clearMDC()
|
||||
}
|
||||
class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], connection: ActorRef, codec: Codec[T]) extends Actor with FSM[TransportHandler.State, TransportHandler.Data] {
|
||||
|
||||
import TransportHandler._
|
||||
|
||||
connection ! Tcp.Register(self)
|
||||
connection ! Tcp.ResumeReading
|
||||
connection ! akka.io.Tcp.Register(self)
|
||||
|
||||
def buf(message: ByteVector): ByteString = ByteString.fromArray(message.toArray)
|
||||
val out = context.actorOf(Props(new WriteAckSender(connection)))
|
||||
|
||||
def buf(message: BinaryData): ByteString = ByteString.fromArray(message)
|
||||
|
||||
// it means we initiate the dialog
|
||||
val isWriter = rs.isDefined
|
||||
@ -89,65 +45,56 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
|
||||
|
||||
val reader = if (isWriter) {
|
||||
val state = makeWriter(keyPair, rs.get)
|
||||
val (state1, message, None) = state.write(ByteVector.empty)
|
||||
val (state1, message, None) = state.write(BinaryData.empty)
|
||||
log.debug(s"sending prefix + $message")
|
||||
connection ! Tcp.Write(buf(TransportHandler.prefix +: message))
|
||||
out ! buf(TransportHandler.prefix +: message)
|
||||
state1
|
||||
} else {
|
||||
makeReader(keyPair)
|
||||
}
|
||||
|
||||
def sendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = {
|
||||
var m: Map[T, Int] = Map()
|
||||
plaintextMessages.foreach(plaintext => Try(codec.decode(plaintext.toBitVector)) match {
|
||||
case Success(Attempt.Successful(DecodeResult(message, _))) =>
|
||||
diag(message, "IN")
|
||||
listener ! message
|
||||
m += (message -> (m.getOrElse(message, 0) + 1))
|
||||
case Success(Attempt.Failure(err)) =>
|
||||
log.error(s"cannot deserialize $plaintext: $err")
|
||||
case Failure(t) =>
|
||||
log.error(s"cannot deserialize $plaintext: ${t.getMessage}")
|
||||
def sendToListener(listener: ActorRef, plaintextMessages: Seq[BinaryData]) = {
|
||||
plaintextMessages.map(plaintext => {
|
||||
codec.decode(BitVector(plaintext.data)) match {
|
||||
case Attempt.Successful(DecodeResult(message, _)) => listener ! message
|
||||
case Attempt.Failure(err) => log.error(s"cannot deserialize $plaintext: $err")
|
||||
}
|
||||
})
|
||||
m
|
||||
}
|
||||
|
||||
startWith(Handshake, HandshakeData(reader))
|
||||
|
||||
when(Handshake) {
|
||||
case Event(Tcp.Received(data), HandshakeData(reader, buffer)) =>
|
||||
connection ! Tcp.ResumeReading
|
||||
log.debug("received {}", ByteVector(data))
|
||||
case Event(Received(data), HandshakeData(reader, buffer)) =>
|
||||
log.debug(s"received ${BinaryData(data)}")
|
||||
val buffer1 = buffer ++ data
|
||||
if (buffer1.length < expectedLength(reader))
|
||||
stay using HandshakeData(reader, buffer1)
|
||||
else {
|
||||
require(buffer1.head == TransportHandler.prefix, s"invalid transport prefix first64=${ByteVector(buffer1.take(64))}")
|
||||
require(buffer1.head == TransportHandler.prefix, s"invalid transport prefix ${buffer1.head}")
|
||||
val (payload, remainder) = buffer1.tail.splitAt(expectedLength(reader) - 1)
|
||||
|
||||
reader.read(ByteVector.view(payload.asByteBuffer)) match {
|
||||
reader.read(payload) match {
|
||||
case (writer, _, Some((dec, enc, ck))) =>
|
||||
val remoteNodeId = PublicKey(writer.rs)
|
||||
remoteNodeId_opt = Some(remoteNodeId)
|
||||
context.parent ! HandshakeCompleted(connection, self, remoteNodeId)
|
||||
val nextStateData = WaitingForListenerData(Encryptor(ExtendedCipherState(enc, ck)), Decryptor(ExtendedCipherState(dec, ck), ciphertextLength = None, remainder))
|
||||
context.parent ! HandshakeCompleted(self, remoteNodeId)
|
||||
val nextStateData = WaitingForListenerData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), remainder)
|
||||
goto(WaitingForListener) using nextStateData
|
||||
|
||||
case (writer, _, None) => {
|
||||
writer.write(ByteVector.empty) match {
|
||||
writer.write(BinaryData.empty) match {
|
||||
case (reader1, message, None) => {
|
||||
// we're still in the middle of the handshake process and the other end must first received our next
|
||||
// message before they can reply
|
||||
require(remainder.isEmpty, "unexpected additional data received during handshake")
|
||||
connection ! Tcp.Write(buf(TransportHandler.prefix +: message))
|
||||
out ! buf(TransportHandler.prefix +: message)
|
||||
stay using HandshakeData(reader1, remainder)
|
||||
}
|
||||
case (_, message, Some((enc, dec, ck))) => {
|
||||
connection ! Tcp.Write(buf(TransportHandler.prefix +: message))
|
||||
out ! buf(TransportHandler.prefix +: message)
|
||||
val remoteNodeId = PublicKey(writer.rs)
|
||||
remoteNodeId_opt = Some(remoteNodeId)
|
||||
context.parent ! HandshakeCompleted(connection, self, remoteNodeId)
|
||||
val nextStateData = WaitingForListenerData(Encryptor(ExtendedCipherState(enc, ck)), Decryptor(ExtendedCipherState(dec, ck), ciphertextLength = None, remainder))
|
||||
context.parent ! HandshakeCompleted(self, remoteNodeId)
|
||||
val nextStateData = WaitingForListenerData(ExtendedCipherState(enc, ck), ExtendedCipherState(dec, ck), remainder)
|
||||
goto(WaitingForListener) using nextStateData
|
||||
}
|
||||
}
|
||||
@ -157,130 +104,59 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
|
||||
}
|
||||
|
||||
when(WaitingForListener) {
|
||||
case Event(Tcp.Received(data), d@WaitingForListenerData(_, dec)) =>
|
||||
stay using d.copy(decryptor = dec.copy(buffer = dec.buffer ++ data))
|
||||
|
||||
case Event(Listener(listener), d@WaitingForListenerData(_, dec)) =>
|
||||
case Event(Received(data), currentStateData@WaitingForListenerData(enc, dec, buffer)) =>
|
||||
stay using currentStateData.copy(buffer = buffer ++ data)
|
||||
|
||||
case Event(Listener(listener), WaitingForListenerData(enc, dec, buffer)) =>
|
||||
val (nextStateData, plaintextMessages) = WaitingForCyphertextData(enc, dec, None, buffer, listener).decrypt
|
||||
context.watch(listener)
|
||||
val (dec1, plaintextMessages) = dec.decrypt()
|
||||
if (plaintextMessages.isEmpty) {
|
||||
connection ! Tcp.ResumeReading
|
||||
goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = Map.empty[T, Int], unackedSent = None)
|
||||
} else {
|
||||
log.debug(s"read ${plaintextMessages.size} messages, waiting for readacks")
|
||||
val unackedReceived = sendToListener(listener, plaintextMessages)
|
||||
goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived, unackedSent = None)
|
||||
}
|
||||
sendToListener(listener, plaintextMessages)
|
||||
goto(WaitingForCyphertext) using nextStateData
|
||||
|
||||
}
|
||||
|
||||
when(Normal) {
|
||||
case Event(Tcp.Received(data), d: NormalData[T]) =>
|
||||
val (dec1, plaintextMessages) = d.decryptor.copy(buffer = d.decryptor.buffer ++ data).decrypt()
|
||||
if (plaintextMessages.isEmpty) {
|
||||
connection ! Tcp.ResumeReading
|
||||
stay using d.copy(decryptor = dec1)
|
||||
} else {
|
||||
log.debug(s"read {} messages, waiting for readacks", plaintextMessages.size)
|
||||
val unackedReceived = sendToListener(d.listener, plaintextMessages)
|
||||
stay using NormalData(d.encryptor, dec1, d.listener, d.sendBuffer, unackedReceived, d.unackedSent)
|
||||
}
|
||||
when(WaitingForCyphertext) {
|
||||
case Event(Received(data), currentStateData@WaitingForCyphertextData(enc, dec, length, buffer, listener)) =>
|
||||
val (nextStateData, plaintextMessages) = WaitingForCyphertextData.decrypt(currentStateData.copy(buffer = buffer ++ data))
|
||||
sendToListener(listener, plaintextMessages)
|
||||
stay using nextStateData
|
||||
|
||||
case Event(ReadAck(msg: T), d: NormalData[T]) =>
|
||||
// how many occurences of this message are still unacked?
|
||||
val remaining = d.unackedReceived.getOrElse(msg, 0) - 1
|
||||
// if all occurences have been acked then we remove the entry from the map
|
||||
val unackedReceived1 = if (remaining > 0) d.unackedReceived + (msg -> remaining) else d.unackedReceived - msg
|
||||
if (unackedReceived1.isEmpty) {
|
||||
log.debug("last incoming message was acked, resuming reading")
|
||||
connection ! Tcp.ResumeReading
|
||||
stay using d.copy(unackedReceived = unackedReceived1)
|
||||
} else {
|
||||
stay using d.copy(unackedReceived = unackedReceived1)
|
||||
}
|
||||
|
||||
case Event(t: T, d: NormalData[T]) =>
|
||||
if (d.sendBuffer.normalPriority.size + d.sendBuffer.lowPriority.size >= MAX_BUFFERED) {
|
||||
log.warning(s"send buffer overrun, closing connection")
|
||||
connection ! PoisonPill
|
||||
stop(FSM.Normal)
|
||||
} else if (d.unackedSent.isDefined) {
|
||||
log.debug("buffering send data={}", t)
|
||||
val sendBuffer1 = t match {
|
||||
case _: ChannelAnnouncement => d.sendBuffer.copy(lowPriority = d.sendBuffer.lowPriority :+ t)
|
||||
case _: NodeAnnouncement => d.sendBuffer.copy(lowPriority = d.sendBuffer.lowPriority :+ t)
|
||||
case _: ChannelUpdate => d.sendBuffer.copy(lowPriority = d.sendBuffer.lowPriority :+ t)
|
||||
case _ => d.sendBuffer.copy(normalPriority = d.sendBuffer.normalPriority :+ t)
|
||||
}
|
||||
stay using d.copy(sendBuffer = sendBuffer1)
|
||||
} else {
|
||||
diag(t, "OUT")
|
||||
val blob = codec.encode(t).require.toByteVector
|
||||
val (enc1, ciphertext) = d.encryptor.encrypt(blob)
|
||||
connection ! Tcp.Write(buf(ciphertext), WriteAck)
|
||||
stay using d.copy(encryptor = enc1, unackedSent = Some(t))
|
||||
}
|
||||
|
||||
case Event(WriteAck, d: NormalData[T]) =>
|
||||
def send(t: T) = {
|
||||
diag(t, "OUT")
|
||||
val blob = codec.encode(t).require.toByteVector
|
||||
val (enc1, ciphertext) = d.encryptor.encrypt(blob)
|
||||
connection ! Tcp.Write(buf(ciphertext), WriteAck)
|
||||
enc1
|
||||
}
|
||||
|
||||
d.sendBuffer.normalPriority.dequeueOption match {
|
||||
case Some((t, normalPriority1)) =>
|
||||
val enc1 = send(t)
|
||||
stay using d.copy(encryptor = enc1, sendBuffer = d.sendBuffer.copy(normalPriority = normalPriority1), unackedSent = Some(t))
|
||||
case None =>
|
||||
d.sendBuffer.lowPriority.dequeueOption match {
|
||||
case Some((t, lowPriority1)) =>
|
||||
val enc1 = send(t)
|
||||
stay using d.copy(encryptor = enc1, sendBuffer = d.sendBuffer.copy(lowPriority = lowPriority1), unackedSent = Some(t))
|
||||
case None =>
|
||||
stay using d.copy(unackedSent = None)
|
||||
}
|
||||
}
|
||||
case Event(t: T, WaitingForCyphertextData(enc, dec, length, buffer, listener)) =>
|
||||
val blob = codec.encode(t).require.toByteArray
|
||||
val (enc1, ciphertext) = TransportHandler.encrypt(enc, blob)
|
||||
out ! buf(ciphertext)
|
||||
stay using WaitingForCyphertextData(enc1, dec, length, buffer, listener)
|
||||
}
|
||||
|
||||
whenUnhandled {
|
||||
case Event(closed: Tcp.ConnectionClosed, _) =>
|
||||
log.info(s"connection closed: $closed")
|
||||
case Event(ErrorClosed(cause), _) =>
|
||||
// we transform connection closed events into application error so that it triggers a uniclose
|
||||
log.warning(s"tcp connection error: $cause")
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(PeerClosed, _) =>
|
||||
log.warning(s"connection closed")
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(Terminated(actor), _) if actor == connection =>
|
||||
log.info(s"connection terminated, stopping the transport")
|
||||
log.warning(s"connection terminated, stopping the transport")
|
||||
// this can be the connection or the listener, either way it is a cause of death
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(msg, d) =>
|
||||
d match {
|
||||
case n: NormalData[T] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}")
|
||||
case _ => log.warning(s"unhandled message $msg in state ${d.getClass.getSimpleName}")
|
||||
}
|
||||
stay
|
||||
}
|
||||
|
||||
override def aroundPostStop(): Unit = connection ! Tcp.Close // attempts to gracefully close the connection when dying
|
||||
override def aroundPostStop(): Unit = connection ! Close
|
||||
|
||||
initialize()
|
||||
|
||||
override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = remoteNodeId_opt)
|
||||
|
||||
}
|
||||
|
||||
object TransportHandler {
|
||||
|
||||
def props[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], connection: ActorRef, codec: Codec[T]): Props = Props(new TransportHandler(keyPair, rs, connection, codec))
|
||||
|
||||
val MAX_BUFFERED = 100000L
|
||||
|
||||
// see BOLT #8
|
||||
// this prefix is prepended to all Noise messages sent during the handshake phase
|
||||
val prefix: Byte = 0x00
|
||||
// this prefix is prepended to all Noise messages sent during the hanshake phase
|
||||
val prefix: Byte = 0
|
||||
|
||||
val prologue = ByteVector.view("lightning".getBytes("UTF-8"))
|
||||
val prologue = "lightning".getBytes("UTF-8")
|
||||
|
||||
/**
|
||||
* See BOLT #8: during the handshake phase we are expecting 3 messages of 50, 50 and 66 bytes (including the prefix)
|
||||
@ -293,30 +169,73 @@ object TransportHandler {
|
||||
case 1 => 66
|
||||
}
|
||||
|
||||
def makeWriter(localStatic: KeyPair, remoteStatic: ByteVector) = Noise.HandshakeState.initializeWriter(
|
||||
/**
|
||||
* see BOLT #8
|
||||
* +-------------------------------
|
||||
* |2-byte encrypted message length|
|
||||
* +-------------------------------
|
||||
* | 16-byte MAC of the encrypted |
|
||||
* | message length |
|
||||
* +-------------------------------
|
||||
* | |
|
||||
* | |
|
||||
* | encrypted lightning |
|
||||
* | message |
|
||||
* | |
|
||||
* +-------------------------------
|
||||
* | 16-byte MAC of the |
|
||||
* | lightning message |
|
||||
* +-------------------------------
|
||||
*
|
||||
* @param enc cipherstate
|
||||
* @param plaintext plaintext
|
||||
* @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8
|
||||
*/
|
||||
def encrypt(enc: CipherState, plaintext: BinaryData): (CipherState, BinaryData) = {
|
||||
val (enc1, ciphertext1) = enc.encryptWithAd(BinaryData.empty, Protocol.writeUInt16(plaintext.length, ByteOrder.BIG_ENDIAN))
|
||||
val (enc2, ciphertext2) = enc1.encryptWithAd(BinaryData.empty, plaintext)
|
||||
(enc2, ciphertext1 ++ ciphertext2)
|
||||
}
|
||||
|
||||
def makeWriter(localStatic: KeyPair, remoteStatic: BinaryData) = Noise.HandshakeState.initializeWriter(
|
||||
Noise.handshakePatternXK, prologue,
|
||||
localStatic, KeyPair(ByteVector.empty, ByteVector.empty), remoteStatic, ByteVector.empty,
|
||||
localStatic, KeyPair(BinaryData.empty, BinaryData.empty), remoteStatic, BinaryData.empty,
|
||||
Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions)
|
||||
|
||||
def makeReader(localStatic: KeyPair) = Noise.HandshakeState.initializeReader(
|
||||
Noise.handshakePatternXK, prologue,
|
||||
localStatic, KeyPair(ByteVector.empty, ByteVector.empty), ByteVector.empty, ByteVector.empty,
|
||||
localStatic, KeyPair(BinaryData.empty, BinaryData.empty), BinaryData.empty, BinaryData.empty,
|
||||
Noise.Secp256k1DHFunctions, Noise.Chacha20Poly1305CipherFunctions, Noise.SHA256HashFunctions)
|
||||
|
||||
// @formatter:off
|
||||
sealed trait State
|
||||
case object Handshake extends State
|
||||
case object WaitingForListener extends State
|
||||
case object WaitingForCyphertext extends State
|
||||
// @formatter:on
|
||||
|
||||
case class Listener(listener: ActorRef)
|
||||
|
||||
case class HandshakeCompleted(transport: ActorRef, remoteNodeId: PublicKey)
|
||||
|
||||
sealed trait Data
|
||||
|
||||
case class HandshakeData(reader: Noise.HandshakeStateReader, buffer: ByteString = ByteString.empty) extends Data
|
||||
|
||||
/**
|
||||
* extended cipher state which implements key rotation as per BOLT #8
|
||||
*
|
||||
* @param cs cipher state
|
||||
* @param ck chaining key
|
||||
*/
|
||||
case class ExtendedCipherState(cs: CipherState, ck: ByteVector) extends CipherState {
|
||||
case class ExtendedCipherState(cs: CipherState, ck: BinaryData) extends CipherState {
|
||||
override def cipher: CipherFunctions = cs.cipher
|
||||
|
||||
override def hasKey: Boolean = cs.hasKey
|
||||
|
||||
override def encryptWithAd(ad: ByteVector, plaintext: ByteVector): (CipherState, ByteVector) = {
|
||||
override def encryptWithAd(ad: BinaryData, plaintext: BinaryData): (CipherState, BinaryData) = {
|
||||
cs match {
|
||||
case UninitializedCipherState(_) => (this, plaintext)
|
||||
case UnitializedCipherState(_) => (this, plaintext)
|
||||
case InitializedCipherState(k, n, _) if n == 999 => {
|
||||
val (_, ciphertext) = cs.encryptWithAd(ad, plaintext)
|
||||
val (ck1, k1) = SHA256HashFunctions.hkdf(ck, k)
|
||||
@ -329,9 +248,9 @@ object TransportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
override def decryptWithAd(ad: ByteVector, ciphertext: ByteVector): (CipherState, ByteVector) = {
|
||||
override def decryptWithAd(ad: BinaryData, ciphertext: BinaryData): (CipherState, BinaryData) = {
|
||||
cs match {
|
||||
case UninitializedCipherState(_) => (this, ciphertext)
|
||||
case UnitializedCipherState(_) => (this, ciphertext)
|
||||
case InitializedCipherState(k, n, _) if n == 999 => {
|
||||
val (_, plaintext) = cs.decryptWithAd(ad, ciphertext)
|
||||
val (ck1, k1) = SHA256HashFunctions.hkdf(ck, k)
|
||||
@ -345,73 +264,29 @@ object TransportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
case class Decryptor(state: CipherState, ciphertextLength: Option[Int], buffer: ByteString) {
|
||||
case class WaitingForListenerData(enc: CipherState, dec: CipherState, buffer: ByteString) extends Data
|
||||
|
||||
case class WaitingForCyphertextData(enc: CipherState, dec: CipherState, ciphertextLength: Option[Int], buffer: ByteString, listener: ActorRef) extends Data {
|
||||
def decrypt: (WaitingForCyphertextData, Seq[BinaryData]) = WaitingForCyphertextData.decrypt(this)
|
||||
}
|
||||
|
||||
object WaitingForCyphertextData {
|
||||
@tailrec
|
||||
final def decrypt(acc: Seq[ByteVector] = Vector()): (Decryptor, Seq[ByteVector]) = {
|
||||
(ciphertextLength, buffer.length) match {
|
||||
case (None, length) if length < 18 => (this, acc)
|
||||
def decrypt(state: WaitingForCyphertextData, acc: Seq[BinaryData] = Nil): (WaitingForCyphertextData, Seq[BinaryData]) = {
|
||||
(state.ciphertextLength, state.buffer.length) match {
|
||||
case (None, length) if length < 18 => (state, acc)
|
||||
case (None, _) =>
|
||||
val (ciphertext, remainder) = buffer.splitAt(18)
|
||||
val (dec1, plaintext) = state.decryptWithAd(ByteVector.empty, ByteVector.view(ciphertext.asByteBuffer))
|
||||
val length = Protocol.uint16(plaintext.toArray, ByteOrder.BIG_ENDIAN)
|
||||
Decryptor(dec1, ciphertextLength = Some(length), buffer = remainder).decrypt(acc)
|
||||
case (Some(expectedLength), length) if length < expectedLength + 16 => (Decryptor(state, ciphertextLength, buffer), acc)
|
||||
val (ciphertext, remainder) = state.buffer.splitAt(18)
|
||||
val (dec1, plaintext) = state.dec.decryptWithAd(BinaryData.empty, ciphertext)
|
||||
val length = Protocol.uint16(plaintext, ByteOrder.BIG_ENDIAN)
|
||||
decrypt(state.copy(dec = dec1, ciphertextLength = Some(length), buffer = remainder), acc)
|
||||
case (Some(expectedLength), length) if length < expectedLength + 16 => (state, acc)
|
||||
case (Some(expectedLength), _) =>
|
||||
val (ciphertext, remainder) = buffer.splitAt(expectedLength + 16)
|
||||
val (dec1, plaintext) = state.decryptWithAd(ByteVector.empty, ByteVector.view(ciphertext.asByteBuffer))
|
||||
Decryptor(dec1, ciphertextLength = None, buffer = remainder).decrypt(acc :+ plaintext)
|
||||
val (ciphertext, remainder) = state.buffer.splitAt(expectedLength + 16)
|
||||
val (dec1, plaintext) = state.dec.decryptWithAd(BinaryData.empty, ciphertext)
|
||||
decrypt(state.copy(dec = dec1, ciphertextLength = None, buffer = remainder), acc :+ plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Encryptor(state: CipherState) {
|
||||
/**
|
||||
* see BOLT #8
|
||||
* +-------------------------------
|
||||
* |2-byte encrypted message length|
|
||||
* +-------------------------------
|
||||
* | 16-byte MAC of the encrypted |
|
||||
* | message length |
|
||||
* +-------------------------------
|
||||
* | |
|
||||
* | |
|
||||
* | encrypted lightning |
|
||||
* | message |
|
||||
* | |
|
||||
* +-------------------------------
|
||||
* | 16-byte MAC of the |
|
||||
* | lightning message |
|
||||
* +-------------------------------
|
||||
*
|
||||
* @param plaintext plaintext
|
||||
* @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8
|
||||
*/
|
||||
def encrypt(plaintext: ByteVector): (Encryptor, ByteVector) = {
|
||||
val (state1, ciphertext1) = state.encryptWithAd(ByteVector.empty, Protocol.writeUInt16(plaintext.length.toInt, ByteOrder.BIG_ENDIAN))
|
||||
val (state2, ciphertext2) = state1.encryptWithAd(ByteVector.empty, plaintext)
|
||||
(Encryptor(state2), ciphertext1 ++ ciphertext2)
|
||||
}
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
sealed trait State
|
||||
case object Handshake extends State
|
||||
case object WaitingForListener extends State
|
||||
case object Normal extends State
|
||||
|
||||
sealed trait Data
|
||||
case class HandshakeData(reader: Noise.HandshakeStateReader, buffer: ByteString = ByteString.empty) extends Data
|
||||
case class WaitingForListenerData(encryptor: Encryptor, decryptor: Decryptor) extends Data
|
||||
case class NormalData[T](encryptor: Encryptor, decryptor: Decryptor, listener: ActorRef, sendBuffer: SendBuffer[T], unackedReceived: Map[T, Int], unackedSent: Option[T]) extends Data
|
||||
|
||||
case class SendBuffer[T](normalPriority: Queue[T], lowPriority: Queue[T])
|
||||
|
||||
case class Listener(listener: ActorRef)
|
||||
|
||||
case class HandshakeCompleted(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey)
|
||||
|
||||
case class ReadAck(msg: Any)
|
||||
case object WriteAck extends Tcp.Event
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
@ -1,56 +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.db
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
|
||||
trait AuditDb {
|
||||
|
||||
def add(availableBalanceChanged: AvailableBalanceChanged)
|
||||
|
||||
def add(channelLifecycle: ChannelLifecycleEvent)
|
||||
|
||||
def add(paymentSent: PaymentSent)
|
||||
|
||||
def add(paymentReceived: PaymentReceived)
|
||||
|
||||
def add(paymentRelayed: PaymentRelayed)
|
||||
|
||||
def add(networkFeePaid: NetworkFeePaid)
|
||||
|
||||
def listSent(from: Long, to: Long): Seq[PaymentSent]
|
||||
|
||||
def listReceived(from: Long, to: Long): Seq[PaymentReceived]
|
||||
|
||||
def listRelayed(from: Long, to: Long): Seq[PaymentRelayed]
|
||||
|
||||
def listNetworkFees(from: Long, to: Long): Seq[NetworkFee]
|
||||
|
||||
def stats: Seq[Stats]
|
||||
|
||||
def close: Unit
|
||||
|
||||
}
|
||||
|
||||
case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacitySat: Long, isFunder: Boolean, isPrivate: Boolean, event: String)
|
||||
|
||||
case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, feeSat: Long, txType: String, timestamp: Long)
|
||||
|
||||
case class Stats(channelId: ByteVector32, avgPaymentAmountSatoshi: Long, paymentCount: Int, relayFeeSatoshi: Long, networkFeeSatoshi: Long)
|
||||
@ -1,36 +1,14 @@
|
||||
/*
|
||||
* 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.db
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.HasCommitments
|
||||
|
||||
trait ChannelsDb {
|
||||
|
||||
def addOrUpdateChannel(state: HasCommitments)
|
||||
|
||||
def removeChannel(channelId: ByteVector32)
|
||||
def removeChannel(channelId: BinaryData)
|
||||
|
||||
def listLocalChannels(): Seq[HasCommitments]
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: Long)
|
||||
|
||||
def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, Long)]
|
||||
|
||||
def close(): Unit
|
||||
def listChannels(): List[HasCommitments]
|
||||
|
||||
}
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.io.File
|
||||
import java.sql.{Connection, DriverManager}
|
||||
|
||||
import fr.acinq.eclair.db.sqlite._
|
||||
|
||||
trait Databases {
|
||||
|
||||
val network: NetworkDb
|
||||
|
||||
val audit: AuditDb
|
||||
|
||||
val channels: ChannelsDb
|
||||
|
||||
val peers: PeersDb
|
||||
|
||||
val payments: PaymentsDb
|
||||
|
||||
val pendingRelay: PendingRelayDb
|
||||
|
||||
}
|
||||
|
||||
object Databases {
|
||||
|
||||
/**
|
||||
* Given a parent folder it creates or loads all the databases from a JDBC connection
|
||||
* @param dbdir
|
||||
* @return
|
||||
*/
|
||||
def sqliteJDBC(dbdir: File): Databases = {
|
||||
dbdir.mkdir()
|
||||
val sqliteEclair = DriverManager.getConnection(s"jdbc:sqlite:${new File(dbdir, "eclair.sqlite")}")
|
||||
val sqliteNetwork = DriverManager.getConnection(s"jdbc:sqlite:${new File(dbdir, "network.sqlite")}")
|
||||
val sqliteAudit = DriverManager.getConnection(s"jdbc:sqlite:${new File(dbdir, "audit.sqlite")}")
|
||||
SqliteUtils.obtainExclusiveLock(sqliteEclair) // there should only be one process writing to this file
|
||||
|
||||
databaseByConnections(sqliteAudit, sqliteNetwork, sqliteEclair)
|
||||
}
|
||||
|
||||
def databaseByConnections(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection) = new Databases {
|
||||
override val network = new SqliteNetworkDb(networkJdbc)
|
||||
override val audit = new SqliteAuditDb(auditJdbc)
|
||||
override val channels = new SqliteChannelsDb(eclairJdbc)
|
||||
override val peers = new SqlitePeersDb(eclairJdbc)
|
||||
override val payments = new SqlitePaymentsDb(eclairJdbc)
|
||||
override val pendingRelay = new SqlitePendingRelayDb(eclairJdbc)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,24 +1,6 @@
|
||||
/*
|
||||
* 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.db
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
|
||||
|
||||
trait NetworkDb {
|
||||
@ -29,33 +11,24 @@ trait NetworkDb {
|
||||
|
||||
def removeNode(nodeId: PublicKey)
|
||||
|
||||
def listNodes(): Seq[NodeAnnouncement]
|
||||
def listNodes(): List[NodeAnnouncement]
|
||||
|
||||
def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi)
|
||||
|
||||
def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Seq(shortChannelId))
|
||||
def addChannel(c: ChannelAnnouncement)
|
||||
|
||||
/**
|
||||
* This method removes channel announcements and associated channel updates for a list of channel ids
|
||||
* This method removes 1 channel announcement and 2 channel updates (at both ends of the same channel)
|
||||
*
|
||||
* @param shortChannelIds list of short channel ids
|
||||
* @param shortChannelId
|
||||
* @return
|
||||
*/
|
||||
def removeChannels(shortChannelIds: Iterable[ShortChannelId])
|
||||
def removeChannel(shortChannelId: Long)
|
||||
|
||||
def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)]
|
||||
def listChannels(): List[ChannelAnnouncement]
|
||||
|
||||
def addChannelUpdate(u: ChannelUpdate)
|
||||
|
||||
def updateChannelUpdate(u: ChannelUpdate)
|
||||
|
||||
def listChannelUpdates(): Seq[ChannelUpdate]
|
||||
|
||||
def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit
|
||||
|
||||
def removeFromPruned(shortChannelId: ShortChannelId)
|
||||
|
||||
def isPruned(shortChannelId: ShortChannelId): Boolean
|
||||
|
||||
def close(): Unit
|
||||
def listChannelUpdates(): List[ChannelUpdate]
|
||||
|
||||
}
|
||||
|
||||
@ -1,85 +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.db
|
||||
|
||||
import java.util.UUID
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
|
||||
trait PaymentsDb {
|
||||
|
||||
// creates a record for a non yet finalized outgoing payment
|
||||
def addOutgoingPayment(outgoingPayment: OutgoingPayment)
|
||||
|
||||
// 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)
|
||||
|
||||
def getOutgoingPayment(id: UUID): Option[OutgoingPayment]
|
||||
|
||||
// all the outgoing payment (attempts) to pay the given paymentHash
|
||||
def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment]
|
||||
|
||||
def listOutgoingPayments(): Seq[OutgoingPayment]
|
||||
|
||||
def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32)
|
||||
|
||||
def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest]
|
||||
|
||||
def getPendingPaymentRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)]
|
||||
|
||||
def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest]
|
||||
|
||||
// returns non paid, non expired payment requests
|
||||
def listPendingPaymentRequests(from: Long, to: Long): Seq[PaymentRequest]
|
||||
|
||||
// assumes there is already a payment request for it (the record for the given payment hash)
|
||||
def addIncomingPayment(payment: IncomingPayment)
|
||||
|
||||
def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment]
|
||||
|
||||
def listIncomingPayments(): 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)
|
||||
|
||||
/**
|
||||
* 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")
|
||||
}
|
||||
@ -1,32 +1,15 @@
|
||||
/*
|
||||
* 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.db
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.wire.NodeAddress
|
||||
|
||||
trait PeersDb {
|
||||
|
||||
def addOrUpdatePeer(nodeId: PublicKey, address: NodeAddress)
|
||||
def addOrUpdatePeer(nodeId: PublicKey, address: InetSocketAddress)
|
||||
|
||||
def removePeer(nodeId: PublicKey)
|
||||
|
||||
def listPeers(): Map[PublicKey, NodeAddress]
|
||||
|
||||
def close(): Unit
|
||||
def listPeers(): List[(PublicKey, InetSocketAddress)]
|
||||
|
||||
}
|
||||
|
||||
@ -1,44 +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.db
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.channel.Command
|
||||
|
||||
/**
|
||||
* This database stores the preimages that we have received from downstream
|
||||
* (either directly via UpdateFulfillHtlc or by extracting the value from the
|
||||
* blockchain).
|
||||
*
|
||||
* This means that this database is only used in the context of *relaying* payments.
|
||||
*
|
||||
* We need to be sure that if downstream is able to pulls funds from us, we can always
|
||||
* do the same from upstream, otherwise we lose money. Hence the need for persistence
|
||||
* to handle all corner cases.
|
||||
*
|
||||
*/
|
||||
trait PendingRelayDb {
|
||||
|
||||
def addPendingRelay(channelId: ByteVector32, htlcId: Long, cmd: Command)
|
||||
|
||||
def removePendingRelay(channelId: ByteVector32, htlcId: Long)
|
||||
|
||||
def listPendingRelay(channelId: ByteVector32): Seq[Command]
|
||||
|
||||
def close(): Unit
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
/**
|
||||
* This database stores the preimages that we have received from downstream
|
||||
* (either directly via UpdateFulfillHtlc or by extracting the value from the
|
||||
* blockchain).
|
||||
*
|
||||
* This means that this database is only used in the context of *relaying* payments.
|
||||
*
|
||||
* We need to be sure that if downstream is able to pulls funds from us, we can always
|
||||
* do the same from upstream, otherwise we lose money. Hence the need for persistence
|
||||
* to handle all corner cases.
|
||||
*
|
||||
*/
|
||||
trait PreimagesDb {
|
||||
|
||||
def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData)
|
||||
|
||||
def removePreimage(channelId: BinaryData, htlcId: Long)
|
||||
|
||||
def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)]
|
||||
|
||||
}
|
||||
@ -1,266 +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.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
import java.util.UUID
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid}
|
||||
import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats}
|
||||
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
|
||||
import fr.acinq.eclair.wire.ChannelCodecs
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
import scala.compat.Platform
|
||||
|
||||
class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
||||
|
||||
import SqliteUtils._
|
||||
import ExtendedResultSet._
|
||||
|
||||
val DB_NAME = "audit"
|
||||
val CURRENT_VERSION = 2
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
|
||||
case 1 => // previous version let's migrate
|
||||
logger.warn(s"Performing db migration for DB $DB_NAME, found version=1 current=$CURRENT_VERSION")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
|
||||
// add id
|
||||
statement.executeUpdate(s"ALTER TABLE sent ADD id BLOB DEFAULT '${ChannelCodecs.UNKNOWN_UUID.toString}' NOT NULL")
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
|
||||
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)")
|
||||
|
||||
// update version
|
||||
setVersion(statement, DB_NAME, CURRENT_VERSION)
|
||||
case CURRENT_VERSION =>
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL, id BLOB NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
|
||||
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)")
|
||||
|
||||
case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
|
||||
}
|
||||
}
|
||||
|
||||
override def add(e: AvailableBalanceChanged): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO balance_updated VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.channelId.toArray)
|
||||
statement.setBytes(2, e.commitments.remoteParams.nodeId.toBin.toArray)
|
||||
statement.setLong(3, e.localBalanceMsat)
|
||||
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(6, Platform.currentTime)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def add(e: ChannelLifecycleEvent): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO channel_events VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.channelId.toArray)
|
||||
statement.setBytes(2, e.remoteNodeId.toBin.toArray)
|
||||
statement.setLong(3, e.capacitySat)
|
||||
statement.setBoolean(4, e.isFunder)
|
||||
statement.setBoolean(5, e.isPrivate)
|
||||
statement.setString(6, e.event)
|
||||
statement.setLong(7, Platform.currentTime)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override def add(e: PaymentRelayed): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, e.amountIn.toLong)
|
||||
statement.setLong(2, e.amountOut.toLong)
|
||||
statement.setBytes(3, e.paymentHash.toArray)
|
||||
statement.setBytes(4, e.fromChannelId.toArray)
|
||||
statement.setBytes(5, e.toChannelId.toArray)
|
||||
statement.setLong(6, e.timestamp)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def add(e: NetworkFeePaid): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO network_fees VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.channelId.toArray)
|
||||
statement.setBytes(2, e.remoteNodeId.toBin.toArray)
|
||||
statement.setBytes(3, e.tx.txid.toArray)
|
||||
statement.setLong(4, e.fee.toLong)
|
||||
statement.setString(5, e.txType)
|
||||
statement.setLong(6, Platform.currentTime)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listSent(from: Long, to: Long): Seq[PaymentSent] =
|
||||
using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND 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"))
|
||||
}
|
||||
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)
|
||||
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"))
|
||||
}
|
||||
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)
|
||||
statement.setLong(2, to)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[PaymentRelayed] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ PaymentRelayed(
|
||||
amountIn = MilliSatoshi(rs.getLong("amount_in_msat")),
|
||||
amountOut = MilliSatoshi(rs.getLong("amount_out_msat")),
|
||||
paymentHash = rs.getByteVector32("payment_hash"),
|
||||
fromChannelId = rs.getByteVector32("from_channel_id"),
|
||||
toChannelId = rs.getByteVector32("to_channel_id"),
|
||||
timestamp = rs.getLong("timestamp"))
|
||||
}
|
||||
q
|
||||
}
|
||||
|
||||
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)
|
||||
statement.setLong(2, to)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[NetworkFee] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ NetworkFee(
|
||||
remoteNodeId = PublicKey(rs.getByteVector("node_id")),
|
||||
channelId = rs.getByteVector32("channel_id"),
|
||||
txId = rs.getByteVector32("tx_id"),
|
||||
feeSat = rs.getLong("fee_sat"),
|
||||
txType = rs.getString("tx_type"),
|
||||
timestamp = rs.getLong("timestamp"))
|
||||
}
|
||||
q
|
||||
}
|
||||
|
||||
override def stats: Seq[Stats] =
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery(
|
||||
"""
|
||||
|SELECT
|
||||
| channel_id,
|
||||
| sum(avg_payment_amount_sat) AS avg_payment_amount_sat,
|
||||
| sum(payment_count) AS payment_count,
|
||||
| sum(relay_fee_sat) AS relay_fee_sat,
|
||||
| sum(network_fee_sat) AS network_fee_sat
|
||||
|FROM (
|
||||
| SELECT
|
||||
| to_channel_id AS channel_id,
|
||||
| avg(amount_out_msat) / 1000 AS avg_payment_amount_sat,
|
||||
| count(*) AS payment_count,
|
||||
| sum(amount_in_msat - amount_out_msat) / 1000 AS relay_fee_sat,
|
||||
| 0 AS network_fee_sat
|
||||
| FROM relayed
|
||||
| GROUP BY 1
|
||||
| UNION
|
||||
| SELECT
|
||||
| channel_id,
|
||||
| 0 AS avg_payment_amount_sat,
|
||||
| 0 AS payment_count,
|
||||
| 0 AS relay_fee_sat,
|
||||
| sum(fee_sat) AS network_fee_sat
|
||||
| FROM network_fees
|
||||
| GROUP BY 1
|
||||
|)
|
||||
|GROUP BY 1
|
||||
""".stripMargin)
|
||||
var q: Queue[Stats] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ Stats(
|
||||
channelId = rs.getByteVector32("channel_id"),
|
||||
avgPaymentAmountSatoshi = rs.getLong("avg_payment_amount_sat"),
|
||||
paymentCount = rs.getInt("payment_count"),
|
||||
relayFeeSatoshi = rs.getLong("relay_fee_sat"),
|
||||
networkFeeSatoshi = rs.getLong("network_fee_sat"))
|
||||
}
|
||||
q
|
||||
}
|
||||
|
||||
override def close(): Unit = sqlite.close()
|
||||
|
||||
}
|
||||
@ -1,107 +1,46 @@
|
||||
/*
|
||||
* 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.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.HasCommitments
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
|
||||
class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
||||
|
||||
import SqliteUtils.ExtendedResultSet._
|
||||
import SqliteUtils._
|
||||
|
||||
val DB_NAME = "channels"
|
||||
val CURRENT_VERSION = 1
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed
|
||||
statement.execute("PRAGMA foreign_keys = ON")
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
|
||||
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)")
|
||||
}
|
||||
|
||||
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 =>
|
||||
update.setBytes(1, data)
|
||||
update.setBytes(2, state.channelId.toArray)
|
||||
if (update.executeUpdate() == 0) {
|
||||
using(sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")) { statement =>
|
||||
statement.setBytes(1, state.channelId.toArray)
|
||||
statement.setBytes(2, data)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def removeChannel(channelId: ByteVector32): Unit = {
|
||||
using(sqlite.prepareStatement("DELETE FROM pending_relay WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement("DELETE FROM htlc_infos WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
val update = sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")
|
||||
update.setBytes(1, data)
|
||||
update.setBytes(2, state.channelId)
|
||||
if (update.executeUpdate() == 0) {
|
||||
val statement = sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")
|
||||
statement.setBytes(1, state.channelId)
|
||||
statement.setBytes(2, data)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override def listLocalChannels(): Seq[HasCommitments] = {
|
||||
using(sqlite.createStatement) { statement =>
|
||||
val rs = statement.executeQuery("SELECT data FROM local_channels")
|
||||
codecSequence(rs, stateDataCodec)
|
||||
}
|
||||
override def removeChannel(channelId: BinaryData): Unit = {
|
||||
val statement1 = sqlite.prepareStatement("DELETE FROM preimages WHERE channel_id=?")
|
||||
statement1.setBytes(1, channelId)
|
||||
statement1.executeUpdate()
|
||||
|
||||
val statement2 = sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")
|
||||
statement2.setBytes(1, channelId)
|
||||
statement2.executeUpdate()
|
||||
}
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: Long): 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.executeUpdate()
|
||||
}
|
||||
override def listChannels(): List[HasCommitments] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM local_channels")
|
||||
codecList(rs, stateDataCodec)
|
||||
}
|
||||
|
||||
def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, Long)] = {
|
||||
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()
|
||||
while (rs.next()) {
|
||||
q = q :+ (ByteVector32(rs.getByteVector32("payment_hash")), rs.getLong("cltv_expiry"))
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
|
||||
override def close(): Unit = sqlite.close
|
||||
}
|
||||
|
||||
@ -1,164 +1,90 @@
|
||||
/*
|
||||
* 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.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.bitcoin.Crypto
|
||||
import fr.acinq.eclair.db.NetworkDb
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec}
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
|
||||
import scodec.bits.BitVector
|
||||
|
||||
class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
|
||||
|
||||
import SqliteUtils._
|
||||
|
||||
val DB_NAME = "network"
|
||||
val CURRENT_VERSION = 1
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
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 channels (short_channel_id INTEGER NOT NULL PRIMARY KEY, data BLOB 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)")
|
||||
}
|
||||
|
||||
override def addNode(n: NodeAnnouncement): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO nodes VALUES (?, ?)")) { statement =>
|
||||
statement.setBytes(1, n.nodeId.toBin.toArray)
|
||||
statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO nodes VALUES (?, ?)")
|
||||
statement.setBytes(1, n.nodeId.toBin)
|
||||
statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def updateNode(n: NodeAnnouncement): Unit = {
|
||||
using(sqlite.prepareStatement("UPDATE nodes SET data=? WHERE node_id=?")) { statement =>
|
||||
statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray)
|
||||
statement.setBytes(2, n.nodeId.toBin.toArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
val statement = sqlite.prepareStatement("UPDATE nodes SET data=? WHERE node_id=?")
|
||||
statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray)
|
||||
statement.setBytes(2, n.nodeId.toBin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def removeNode(nodeId: Crypto.PublicKey): Unit = {
|
||||
using(sqlite.prepareStatement("DELETE FROM nodes WHERE node_id=?")) { statement =>
|
||||
statement.setBytes(1, nodeId.toBin.toArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
val statement = sqlite.prepareStatement("DELETE FROM nodes WHERE node_id=?")
|
||||
statement.setBytes(1, nodeId.toBin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listNodes(): Seq[NodeAnnouncement] = {
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery("SELECT data FROM nodes")
|
||||
codecSequence(rs, nodeAnnouncementCodec)
|
||||
}
|
||||
override def listNodes(): List[NodeAnnouncement] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM nodes")
|
||||
codecList(rs, nodeAnnouncementCodec)
|
||||
}
|
||||
|
||||
override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, c.shortChannelId.toLong)
|
||||
statement.setString(2, txid.toHex)
|
||||
statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray)
|
||||
statement.setLong(4, capacity.amount)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
override def addChannel(c: ChannelAnnouncement): Unit = {
|
||||
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?)")
|
||||
statement.setLong(1, c.shortChannelId)
|
||||
statement.setBytes(2, channelAnnouncementCodec.encode(c).require.toByteArray)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// remove channels by batch of 1000
|
||||
shortChannelIds.grouped(1000).foreach(removeChannelsInternal)
|
||||
override def removeChannel(shortChannelId: Long): Unit = {
|
||||
val statement = sqlite.createStatement
|
||||
statement.execute("BEGIN TRANSACTION")
|
||||
statement.executeUpdate(s"DELETE FROM channel_updates WHERE short_channel_id=$shortChannelId")
|
||||
statement.executeUpdate(s"DELETE FROM channels WHERE short_channel_id=$shortChannelId")
|
||||
statement.execute("COMMIT TRANSACTION")
|
||||
}
|
||||
|
||||
override def listChannels(): Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = {
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery("SELECT data, txid, capacity_sat FROM channels")
|
||||
var m: Map[ChannelAnnouncement, (ByteVector32, Satoshi)] = Map()
|
||||
while (rs.next()) {
|
||||
m += (channelAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value ->
|
||||
(ByteVector32.fromValidHex(rs.getString("txid")), Satoshi(rs.getLong("capacity_sat"))))
|
||||
}
|
||||
m
|
||||
}
|
||||
override def listChannels(): List[ChannelAnnouncement] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channels")
|
||||
codecList(rs, channelAnnouncementCodec)
|
||||
}
|
||||
|
||||
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.setBytes(3, channelUpdateCodec.encode(u).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO channel_updates VALUES (?, ?, ?)")
|
||||
statement.setLong(1, u.shortChannelId)
|
||||
statement.setBoolean(2, Announcements.isNode1(u.flags))
|
||||
statement.setBytes(3, channelUpdateCodec.encode(u).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def updateChannelUpdate(u: ChannelUpdate): Unit = {
|
||||
using(sqlite.prepareStatement("UPDATE channel_updates SET data=? WHERE short_channel_id=? AND node_flag=?")) { statement =>
|
||||
statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray)
|
||||
statement.setLong(2, u.shortChannelId.toLong)
|
||||
statement.setBoolean(3, Announcements.isNode1(u.channelFlags))
|
||||
statement.executeUpdate()
|
||||
}
|
||||
val statement = sqlite.prepareStatement("UPDATE channel_updates SET data=? WHERE short_channel_id=? AND node_flag=?")
|
||||
statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray)
|
||||
statement.setLong(2, u.shortChannelId)
|
||||
statement.setBoolean(3, Announcements.isNode1(u.flags))
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listChannelUpdates(): Seq[ChannelUpdate] = {
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery("SELECT data FROM channel_updates")
|
||||
codecSequence(rs, channelUpdateCodec)
|
||||
}
|
||||
override def listChannelUpdates(): List[ChannelUpdate] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channel_updates")
|
||||
codecList(rs, channelUpdateCodec)
|
||||
}
|
||||
|
||||
override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), disableAutoCommit = true) { statement =>
|
||||
shortChannelIds.foreach(shortChannelId => {
|
||||
statement.setLong(1, shortChannelId.toLong)
|
||||
statement.addBatch()
|
||||
})
|
||||
statement.executeBatch()
|
||||
}
|
||||
}
|
||||
|
||||
override def removeFromPruned(shortChannelId: ShortChannelId): Unit = {
|
||||
using(sqlite.createStatement) { statement =>
|
||||
statement.executeUpdate(s"DELETE FROM pruned WHERE short_channel_id=${shortChannelId.toLong}")
|
||||
}
|
||||
}
|
||||
|
||||
override def isPruned(shortChannelId: ShortChannelId): Boolean = {
|
||||
using(sqlite.prepareStatement("SELECT short_channel_id from pruned WHERE short_channel_id=?")) { statement =>
|
||||
statement.setLong(1, shortChannelId.toLong)
|
||||
val rs = statement.executeQuery()
|
||||
rs.next()
|
||||
}
|
||||
}
|
||||
|
||||
override def close(): Unit = sqlite.close
|
||||
}
|
||||
|
||||
@ -1,227 +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.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils._
|
||||
import fr.acinq.eclair.db.{IncomingPayment, OutgoingPayment, OutgoingPaymentStatus, PaymentsDb}
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import grizzled.slf4j.Logging
|
||||
import scala.collection.immutable.Queue
|
||||
import OutgoingPaymentStatus._
|
||||
|
||||
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) // 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)
|
||||
}
|
||||
|
||||
override def addOutgoingPayment(sent: OutgoingPayment): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) 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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, preimage, status) = (?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
|
||||
statement.setLong(1, Instant.now().getEpochSecond)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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 =>
|
||||
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"))
|
||||
))
|
||||
} 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 (?, ?, ?, ?)"
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement(insertStmt)) { statement =>
|
||||
statement.setBytes(1, pr.paymentHash.toArray)
|
||||
statement.setBytes(2, preimage.toArray)
|
||||
statement.setString(3, PaymentRequest.write(pr))
|
||||
statement.setLong(4, pr.timestamp)
|
||||
pr.expiry.foreach { ex => statement.setLong(5, pr.timestamp + ex) } // we store "when" the invoice will expire
|
||||
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 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 =>
|
||||
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)
|
||||
} 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)
|
||||
statement.setLong(2, to)
|
||||
if (pendingOnly) statement.setLong(3, Instant.now().getEpochSecond)
|
||||
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[PaymentRequest] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ PaymentRequest.read(rs.getString("payment_request"))
|
||||
}
|
||||
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 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)
|
||||
val rs = statement.executeQuery()
|
||||
if (rs.next()) {
|
||||
Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
var q: Queue[IncomingPayment] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,76 +1,46 @@
|
||||
/*
|
||||
* 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.db.sqlite
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.Crypto
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.db.PeersDb
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs.socketaddress
|
||||
import scodec.bits.BitVector
|
||||
|
||||
class SqlitePeersDb(sqlite: Connection) extends PeersDb {
|
||||
|
||||
import SqliteUtils.ExtendedResultSet._
|
||||
|
||||
val DB_NAME = "peers"
|
||||
val CURRENT_VERSION = 1
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
|
||||
}
|
||||
|
||||
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, nodeaddress: NodeAddress): Unit = {
|
||||
val data = LightningMessageCodecs.nodeaddress.encode(nodeaddress).require.toByteArray
|
||||
using(sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")) { update =>
|
||||
update.setBytes(1, data)
|
||||
update.setBytes(2, nodeId.toBin.toArray)
|
||||
if (update.executeUpdate() == 0) {
|
||||
using(sqlite.prepareStatement("INSERT INTO peers VALUES (?, ?)")) { statement =>
|
||||
statement.setBytes(1, nodeId.toBin.toArray)
|
||||
statement.setBytes(2, data)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def removePeer(nodeId: Crypto.PublicKey): Unit = {
|
||||
using(sqlite.prepareStatement("DELETE FROM peers WHERE node_id=?")) { statement =>
|
||||
statement.setBytes(1, nodeId.toBin.toArray)
|
||||
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: InetSocketAddress): Unit = {
|
||||
val data = socketaddress.encode(address).require.toByteArray
|
||||
val update = sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")
|
||||
update.setBytes(1, data)
|
||||
update.setBytes(2, nodeId.toBin)
|
||||
if (update.executeUpdate() == 0) {
|
||||
val statement = sqlite.prepareStatement("INSERT INTO peers VALUES (?, ?)")
|
||||
statement.setBytes(1, nodeId.toBin)
|
||||
statement.setBytes(2, data)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override def listPeers(): Map[PublicKey, NodeAddress] = {
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery("SELECT node_id, data FROM peers")
|
||||
var m: Map[PublicKey, NodeAddress] = Map()
|
||||
while (rs.next()) {
|
||||
val nodeid = PublicKey(rs.getByteVector("node_id"))
|
||||
val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value
|
||||
m += (nodeid -> nodeaddress)
|
||||
}
|
||||
m
|
||||
}
|
||||
override def removePeer(nodeId: Crypto.PublicKey): Unit = {
|
||||
val statement = sqlite.prepareStatement("DELETE FROM peers WHERE node_id=?")
|
||||
statement.setBytes(1, nodeId.toBin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def close(): Unit = sqlite.close()
|
||||
override def listPeers(): List[(PublicKey, InetSocketAddress)] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT node_id, data FROM peers")
|
||||
var l: List[(PublicKey, InetSocketAddress)] = Nil
|
||||
while (rs.next()) {
|
||||
l = l :+ (PublicKey(rs.getBytes("node_id")), socketaddress.decode(BitVector(rs.getBytes("data"))).require.value)
|
||||
}
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,64 +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.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.channel.Command
|
||||
import fr.acinq.eclair.db.PendingRelayDb
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils.{codecSequence, getVersion, using}
|
||||
import fr.acinq.eclair.wire.CommandCodecs.cmdCodec
|
||||
|
||||
class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb {
|
||||
|
||||
val DB_NAME = "pending_relay"
|
||||
val CURRENT_VERSION = 1
|
||||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // 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))")
|
||||
}
|
||||
|
||||
override def addPendingRelay(channelId: ByteVector32, htlcId: Long, cmd: Command): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO pending_relay VALUES (?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.setBytes(3, cmdCodec.encode(cmd).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override def removePendingRelay(channelId: ByteVector32, htlcId: Long): Unit = {
|
||||
using(sqlite.prepareStatement("DELETE FROM pending_relay WHERE channel_id=? AND htlc_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override def listPendingRelay(channelId: ByteVector32): Seq[Command] = {
|
||||
using(sqlite.prepareStatement("SELECT htlc_id, data FROM pending_relay WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
val rs = statement.executeQuery()
|
||||
codecSequence(rs, cmdCodec)
|
||||
}
|
||||
}
|
||||
|
||||
override def close(): Unit = sqlite.close()
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.db.PreimagesDb
|
||||
|
||||
class SqlitePreimagesDb(sqlite: Connection) extends PreimagesDb {
|
||||
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
// note: should we use a foreign key to local_channels table here?
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS preimages (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, preimage BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))")
|
||||
}
|
||||
|
||||
override def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData): Unit = {
|
||||
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO preimages VALUES (?, ?, ?)")
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.setBytes(3, paymentPreimage)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def removePreimage(channelId: BinaryData, htlcId: Long): Unit = {
|
||||
val statement = sqlite.prepareStatement("DELETE FROM preimages WHERE channel_id=? AND htlc_id=?")
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)] = {
|
||||
val statement = sqlite.prepareStatement("SELECT htlc_id, preimage FROM preimages WHERE channel_id=?")
|
||||
statement.setBytes(1, channelId)
|
||||
val rs = statement.executeQuery()
|
||||
var l: List[(BinaryData, Long, BinaryData)] = Nil
|
||||
while (rs.next()) {
|
||||
l = l :+ (channelId, rs.getLong("htlc_id"), BinaryData(rs.getBytes("preimage")))
|
||||
}
|
||||
l
|
||||
}
|
||||
}
|
||||
@ -1,79 +1,12 @@
|
||||
/*
|
||||
* 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.db.sqlite
|
||||
|
||||
import java.sql.{Connection, ResultSet, Statement}
|
||||
import java.sql.ResultSet
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import scodec.Codec
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
import scodec.bits.BitVector
|
||||
|
||||
object SqliteUtils {
|
||||
|
||||
/**
|
||||
* Manages closing of statement
|
||||
*
|
||||
* @param statement
|
||||
* @param block
|
||||
*/
|
||||
def using[T <: Statement, U](statement: T, disableAutoCommit: Boolean = false)(block: T => U): U = {
|
||||
try {
|
||||
if (disableAutoCommit) statement.getConnection.setAutoCommit(false)
|
||||
block(statement)
|
||||
} finally {
|
||||
if (disableAutoCommit) 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
|
||||
*/
|
||||
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
|
||||
statement.executeUpdate(s"INSERT OR IGNORE INTO versions VALUES ('$db_name', $currentVersion)")
|
||||
// if there was a previous version installed, this will return a different value from current version
|
||||
val res = statement.executeQuery(s"SELECT version FROM versions WHERE db_name='$db_name'")
|
||||
res.getInt("version")
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the version for a particular logical database, it will overwrite the previous version.
|
||||
* @param statement
|
||||
* @param db_name
|
||||
* @param newVersion
|
||||
* @return
|
||||
*/
|
||||
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
|
||||
statement.executeUpdate(s"UPDATE versions SET version=$newVersion WHERE db_name='$db_name'")
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper assumes that there is a "data" column available, decodable with the provided codec
|
||||
*
|
||||
@ -84,56 +17,11 @@ object SqliteUtils {
|
||||
* @tparam T
|
||||
* @return
|
||||
*/
|
||||
def codecSequence[T](rs: ResultSet, codec: Codec[T]): Seq[T] = {
|
||||
var q: Queue[T] = Queue()
|
||||
def codecList[T](rs: ResultSet, codec: Codec[T]): List[T] = {
|
||||
var l: List[T] = Nil
|
||||
while (rs.next()) {
|
||||
q = q :+ codec.decode(BitVector(rs.getBytes("data"))).require.value
|
||||
l = l :+ codec.decode(BitVector(rs.getBytes("data"))).require.value
|
||||
}
|
||||
q
|
||||
}
|
||||
|
||||
/**
|
||||
* 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] = {
|
||||
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 explicitely reset.
|
||||
*
|
||||
* @param sqlite
|
||||
*/
|
||||
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
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS dummy_table_for_locking (a INTEGER NOT NULL)")
|
||||
statement.executeUpdate("INSERT INTO dummy_table_for_locking VALUES (42)")
|
||||
}
|
||||
|
||||
case class ExtendedResultSet(rs: ResultSet) {
|
||||
|
||||
def getByteVector(columnLabel: String): ByteVector = ByteVector(rs.getBytes(columnLabel))
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
object ExtendedResultSet {
|
||||
implicit def conv(rs: ResultSet): ExtendedResultSet = ExtendedResultSet(rs)
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user