Compare commits

..

1 Commits

Author SHA1 Message Date
pm47
00894bae64 moved akka conf to eclair-node application.conf 2017-11-24 16:15:12 +01:00
300 changed files with 12835 additions and 35417 deletions

View File

@ -1,6 +0,0 @@
Dockerfile
.dockerignore
.git
**/*.idea
**/*.iml
**/target

View File

@ -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. -->

View File

@ -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

View File

@ -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`

View File

@ -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"]

View File

@ -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.

View File

@ -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
View File

@ -2,21 +2,20 @@
[![Build Status](https://travis-ci.org/ACINQ/eclair.svg?branch=master)](https://travis-ci.org/ACINQ/eclair)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-red.svg)](https://gitter.im/ACINQ/eclair)
[![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-rose.svg)](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
![Eclair Demo](.readme/screen-1.png)
## 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
View File

@ -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).

View File

@ -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

View File

@ -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
View 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

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
}

View File

@ -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
}
}

View File

@ -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.")

View File

@ -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)
)
}

View File

@ -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}

View File

@ -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))
}

View File

@ -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.

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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 {

View File

@ -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))

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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())
}
))

View File

@ -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
}

View File

@ -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))
}
}
}
}
}
}
}
}
}

View File

@ -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}

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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]
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))
}*/

View File

@ -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")
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"))
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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")
}
}
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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),

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)))
}

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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}

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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]
}

View File

@ -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)
}
}

View File

@ -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]
}

View File

@ -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")
}

View File

@ -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)]
}

View File

@ -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
}

View File

@ -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)]
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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