Compare commits
1 Commits
applicatio
...
wip-remote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b467ed89d0 |
@ -2,7 +2,7 @@ sudo: required
|
||||
dist: trusty
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.11
|
||||
- 2.11.8
|
||||
env:
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib
|
||||
script:
|
||||
|
||||
7
BUILD.md
7
BUILD.md
@ -10,12 +10,7 @@ To build the project, simply run:
|
||||
```shell
|
||||
$ mvn package
|
||||
```
|
||||
To skip the tests, run:
|
||||
or
|
||||
```shell
|
||||
$ mvn package -DskipTests
|
||||
```
|
||||
To generate the windows installer along with the build, run the following command:
|
||||
```shell
|
||||
$ mvn package -DskipTests -Pinstaller
|
||||
```
|
||||
The generated installer will be located in `eclair-node-gui/target/jfx/installer`
|
||||
|
||||
81
README.md
81
README.md
@ -13,8 +13,6 @@ This software follows the [Lightning Network Specifications (BOLTs)](https://git
|
||||
:construction: Both the BOLTs and Eclair itself are a work in progress. Expect things to break/change!
|
||||
|
||||
: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!
|
||||
|
||||
---
|
||||
|
||||
@ -27,11 +25,11 @@ Please see the latest [release note](https://github.com/ACINQ/eclair/releases) f
|
||||
|
||||
## 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)**.
|
||||
:warning: **Those are valid for the most up-to-date, unreleased, version of eclair. Here are the [instructions for Eclair 0.2-alpha2](https://github.com/ACINQ/eclair/blob/v0.2-alpha2/README.md#installation)**.
|
||||
|
||||
### Configuring Bitcoin Core
|
||||
|
||||
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+.
|
||||
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.0.
|
||||
|
||||
Run bitcoind with the following minimal `bitcoin.conf`:
|
||||
```
|
||||
@ -46,8 +44,6 @@ zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
|
||||
### Installing Eclair
|
||||
|
||||
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).
|
||||
|
||||
#### Windows
|
||||
|
||||
Just use the windows installer, it should create a shortcut on your desktop.
|
||||
@ -56,79 +52,52 @@ Just use the windows installer, it should create a shortcut on your desktop.
|
||||
|
||||
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).
|
||||
:warning: If you are using the OpenJDK JRE, you will need to build OpenJFX yourself, or run the application in `--headless` mode.
|
||||
|
||||
Then download the latest fat jar and depending on whether or not you want a GUI run the following command:
|
||||
* with GUI:
|
||||
Then download the latest fat jar and run:
|
||||
```shell
|
||||
java -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
```
|
||||
* without GUI:
|
||||
```shell
|
||||
java -jar eclair-node-<version>-<commit_id>.jar
|
||||
java -jar eclair-node_xxxxxx-fat.jar
|
||||
```
|
||||
|
||||
### Configuring Eclair
|
||||
|
||||
#### Command-line parameters
|
||||
|
||||
option | description | default value
|
||||
---------------|---------------------------------|--------------
|
||||
--datadir | Path to the data directory | ~/.eclair
|
||||
--headless | Run Eclair without the GUI |
|
||||
--help, -h | Display usage text |
|
||||
|
||||
|
||||
: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 the configuration (see below).
|
||||
|
||||
#### Configuration file
|
||||
|
||||
Eclair reads its configuration file, and write its logs, to a `datadir` directory, located in `~/.eclair` by default.
|
||||
To change your node configuration, edit the file `eclair.conf` in `datadir`.
|
||||
|
||||
To change your node's configuration, create a file named `eclair.conf` in `datadir`. Here's an example configuration file:
|
||||
|
||||
```
|
||||
eclair.server.port=9735
|
||||
eclair.node-alias=eclair
|
||||
eclair.node-color=49daaa
|
||||
```
|
||||
|
||||
Here are some of the most common options:
|
||||
|
||||
name | description | default value
|
||||
option | description | default value
|
||||
-----------------------------|---------------------------|--------------
|
||||
eclair.server.port | Lightning TCP port | 9735
|
||||
eclair.api.port | API HTTP port | 8080
|
||||
eclair.server.port | TCP port | 9735
|
||||
eclair.api.port | 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).
|
||||
|
||||
→ see [`reference.conf`](eclair-core/src/main/resources/reference.conf) for full reference. There are many more options!
|
||||
|
||||
#### Java Environment Variables
|
||||
|
||||
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).
|
||||
|
||||
name | description | default value
|
||||
----------------------|--------------------------------------------|--------------
|
||||
eclair.datadir | Path to the data directory | ~/.eclair
|
||||
eclair.headless | Run eclair without a GUI |
|
||||
eclair.printToConsole | Log to stdout (in addition to eclair.log) |
|
||||
|
||||
For example, to specify a different data directory you would run the following command:
|
||||
```shell
|
||||
java -Declair.datadir=/tmp/node1 -jar eclair-node-gui-<version>-<commit_id>.jar
|
||||
```
|
||||
→ see [`application.conf`](eclair-node/src/main/resources/application.conf) for full reference.
|
||||
|
||||
## JSON-RPC API
|
||||
|
||||
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
|
||||
connect | host, port, pubkey | connect to another lightning node through a secure connection
|
||||
open | host, port, pubkey, 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
|
||||
network | | list all nodes that have been announced
|
||||
genh | | generate a payment H
|
||||
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
|
||||
|
||||
120
TESTING.md
Normal file
120
TESTING.md
Normal file
@ -0,0 +1,120 @@
|
||||
# Testing eclair and lightningd
|
||||
|
||||
## Configure bitcoind to run in regtest mode
|
||||
Important: you need a segwit version of bitcoin core for this test (see https://github.com/sipa/bitcoin/tree/segwit-master).
|
||||
Make sure that bitcoin-cli is on the path and edit ~/.bitcoin/bitcoin.conf and add:
|
||||
```shell
|
||||
server=1
|
||||
regtest=1
|
||||
rpcuser=***
|
||||
rpcpassword=***
|
||||
```
|
||||
|
||||
To check that segwit is enabled run:
|
||||
```shell
|
||||
bitcoin-cli getblockchaininfo
|
||||
```
|
||||
and check bip9_softforks:
|
||||
|
||||
```
|
||||
...
|
||||
"bip9_softforks": {
|
||||
"csv": {
|
||||
"status": "active",
|
||||
"startTime": 0,
|
||||
"timeout": 999999999999
|
||||
},
|
||||
"witness": {
|
||||
"status": "active",
|
||||
"startTime": 0,
|
||||
"timeout": 999999999999
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Start bitcoind
|
||||
Mine enough blocks to activate segwit blocks:
|
||||
```shell
|
||||
bitcoin-cli generate 500
|
||||
```
|
||||
##
|
||||
Start lightningd (here we’ll use port 46000)
|
||||
```shell
|
||||
lightningd --port 46000
|
||||
```
|
||||
##
|
||||
Start eclair:
|
||||
```shell
|
||||
mvn exec:java -Dexec.mainClass=fr.acinq.eclair.Boot
|
||||
```
|
||||
## Tell eclair to connect to lightningd
|
||||
|
||||
```shell
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"method": "connect",
|
||||
"params" : [ "localhost", 46000, 3000000 ]
|
||||
}' http://localhost:8080
|
||||
```
|
||||
Since eclair is funder, it will create and publish the funding tx
|
||||
|
||||
Mine a few blocks to confirm the funding tx:
|
||||
```shell
|
||||
bitcoin-cli generate 10
|
||||
```
|
||||
eclair and lightningd are now both in NORMAL state.
|
||||
You can check this by running:
|
||||
```shell
|
||||
lightning-cli getpeers
|
||||
```
|
||||
or
|
||||
```shell
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"method": "list",
|
||||
"params" : [ ]
|
||||
}' http://localhost:8080
|
||||
```
|
||||
|
||||
|
||||
## Tell eclair to send a htlc
|
||||
We’ll use the following values for R and H:
|
||||
```
|
||||
R = 0102030405060708010203040506070801020304050607080102030405060708
|
||||
H = 8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609
|
||||
```
|
||||
|
||||
You’ll need a unix timestamp that is not too far into the future. Now + 100000 is fine:
|
||||
```shell
|
||||
curl -X POST -H "Content-Type: application/json" -d "{
|
||||
\"method\": \"addhtlc\",
|
||||
\"params\" : [ 70000000, \"8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609\", $((`date +%s` + 100000)), \"021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4\" ]
|
||||
}" http://localhost:8080
|
||||
```
|
||||
|
||||
## Tell eclair to commit its changes
|
||||
```shell
|
||||
curl -X POST -H "Content-Type: application/json" -d "{
|
||||
\"method\": \"sign\",
|
||||
\"params\" : [ \"d3f056a084e266ad06ea1ca28a1e080ca07c6b61fac7ce116e48a5c31d688eee\" ]
|
||||
}" http://localhost:8080
|
||||
```
|
||||
## Tell lightningd to fulfill the HTLC:
|
||||
```shell
|
||||
./lightning-cli fulfillhtlc 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb 0102030405060708010203040506070801020304050607080102030405060708
|
||||
```
|
||||
Check balances on both eclair and lightningd
|
||||
|
||||
## Close the channel
|
||||
```shell
|
||||
./lightning-cli close 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb
|
||||
```
|
||||
Mine a few blocks to bury the closing tx
|
||||
```shell
|
||||
bitcoin-cli generate 10
|
||||
```
|
||||
The channel is now in CLOSED state
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
[ -z "$1" ] && (
|
||||
echo "usage: "
|
||||
echo " eclair-cli help"
|
||||
) && exit 1
|
||||
|
||||
URL="http://localhost:8080"
|
||||
CURL_OPTS="-sS -X POST -H \"Content-Type: application/json\""
|
||||
|
||||
case $1 in
|
||||
"help")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"help\", \"params\" : [] }' $URL" | jq -r ".result[]"
|
||||
;;
|
||||
"getinfo")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"getinfo\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"channels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channels\", \"params\" : [] }' $URL" | jq ".result[]"
|
||||
;;
|
||||
"channel")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"channel\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL" | jq ".result | { nodeid, channelId, state, balanceMsat: .data.commitments.localCommit.spec.toLocalMsat, capacitySat: .data.commitments.commitInput.txOut.amount.amount }"
|
||||
;;
|
||||
"open")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"open\", \"params\" : [\"${2?"missing node id"}\", \"${3?"missing ip"}\", ${4?"missing port"}, ${5?"missing amount (sat)"}, ${6?"missing push amount (msat)"}] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"close")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
|
||||
;;
|
||||
"receive")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"send")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"network")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"peers")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
esac
|
||||
@ -1,216 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-core_2.11</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
|
||||
<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>
|
||||
<version>1.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>download-bitcoind</id>
|
||||
<phase>generate-test-resources</phase>
|
||||
<goals>
|
||||
<goal>wget</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<url>${bitcoind.url}</url>
|
||||
<unpack>true</unpack>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<md5>${bitcoind.md5}</md5>
|
||||
<sha1>${bitcoind.sha1}</sha1>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<!-- we hide the git commit in the Specification-Version standard field-->
|
||||
<Specification-Version>${git.commit.id}</Specification-Version>
|
||||
<Url>${project.parent.url}</Url>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>default</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<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>c811c157d4d618f7d7f4b9f24834551c</bitcoind.md5>
|
||||
<bitcoind.sha1>3ab7e537bd00bf35e6a78fca108d0d886f8289c1</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>Mac</id>
|
||||
<activation>
|
||||
<os>
|
||||
<family>mac</family>
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-osx64.tar.gz
|
||||
</bitcoind.url>
|
||||
<bitcoind.md5>1521e1d0901169004b9c1c9b552868b7</bitcoind.md5>
|
||||
<bitcoind.sha1>7216298f77162618f322fdf499f1f1b67a0048b7</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>Windows</id>
|
||||
<activation>
|
||||
<os>
|
||||
<family>Windows</family>
|
||||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<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>
|
||||
|
||||
<dependencies>
|
||||
<!-- AKKA -->
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-actor_${scala.version.short}</artifactId>
|
||||
<version>${akka.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-slf4j_${scala.version.short}</artifactId>
|
||||
<version>${akka.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-http-core_${scala.version.short}</artifactId>
|
||||
<version>10.0.7</version>
|
||||
</dependency>
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>org.json4s</groupId>
|
||||
<artifactId>json4s-jackson_${scala.version.short}</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.heikoseeberger</groupId>
|
||||
<artifactId>akka-http-json4s_${scala.version.short}</artifactId>
|
||||
<version>1.16.1</version>
|
||||
</dependency>
|
||||
<!-- BITCOIN -->
|
||||
<dependency>
|
||||
<groupId>fr.acinq</groupId>
|
||||
<artifactId>bitcoin-lib_${scala.version.short}</artifactId>
|
||||
<version>${bitcoinlib.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.zeromq</groupId>
|
||||
<artifactId>jeromq</artifactId>
|
||||
<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.10.3</version>
|
||||
</dependency>
|
||||
<!-- LOGGING -->
|
||||
<dependency>
|
||||
<groupId>org.clapper</groupId>
|
||||
<artifactId>grizzled-slf4j_${scala.version.short}</artifactId>
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
<!-- OTHER -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<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 -->
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<!-- TESTS -->
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
<artifactId>akka-testkit_${scala.version.short}</artifactId>
|
||||
<version>${akka.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"127.0.0.1": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
},
|
||||
"10.0.2.2": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"testnetnode.arihanc.com": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
},
|
||||
"testnet.hsmiths.com": {
|
||||
"t": "53011",
|
||||
"s": "53012"
|
||||
},
|
||||
"electrum.akinbo.org": {
|
||||
"t": "51001",
|
||||
"s": "51002"
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
eclair {
|
||||
|
||||
chain = "test" // "regtest" for regtest, "test" for testnet. Livenet is not supported.
|
||||
|
||||
server {
|
||||
public-ips = [] // external ips, will be announced on the network
|
||||
binding-ip = "0.0.0.0"
|
||||
port = 9735
|
||||
}
|
||||
|
||||
api {
|
||||
binding-ip = "127.0.0.1"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
|
||||
|
||||
bitcoind {
|
||||
host = "localhost"
|
||||
rpcport = 18332
|
||||
rpcuser = "foo"
|
||||
rpcpassword = "bar"
|
||||
zmq = "tcp://127.0.0.1:29000"
|
||||
}
|
||||
|
||||
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 = 210
|
||||
2 = 180
|
||||
6 = 150
|
||||
12 = 110
|
||||
36 = 50
|
||||
72 = 20
|
||||
}
|
||||
}
|
||||
|
||||
node-alias = "eclair"
|
||||
node-color = "49daaa"
|
||||
global-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
|
||||
|
||||
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%)
|
||||
|
||||
delay-blocks = 144
|
||||
mindepth-blocks = 2
|
||||
expiry-delta-blocks = 144
|
||||
|
||||
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
|
||||
max-feerate-mismatch = 1.5
|
||||
|
||||
// funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater
|
||||
// than this ratio.
|
||||
update-fee_min-diff-ratio = 0.1
|
||||
|
||||
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
|
||||
auto-reconnect = true
|
||||
|
||||
payment-handler = "local"
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object DBCompatChecker extends Logging {
|
||||
|
||||
/**
|
||||
* 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.networkDb.listChannels() ++ nodeParams.networkDb.listNodes() ++ nodeParams.peersDb.listPeers() ++ nodeParams.channelsDb.listChannels()) match {
|
||||
case Success(_) => {}
|
||||
case Failure(_) => throw IncompatibleDBException
|
||||
}
|
||||
}
|
||||
|
||||
case object IncompatibleDBException extends RuntimeException("DB files are not compatible with this version of eclair.")
|
||||
@ -1,26 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import akka.actor.{Actor, FSM}
|
||||
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
|
||||
|
||||
/**
|
||||
* A version of akka.actor.DiagnosticActorLogging compatible with an FSM
|
||||
* See https://groups.google.com/forum/#!topic/akka-user/0CxR8CImr4Q
|
||||
*/
|
||||
trait FSMDiagnosticActorLogging[S, D] extends FSM[S, D] {
|
||||
|
||||
import akka.event.Logging._
|
||||
|
||||
val diagLog: DiagnosticLoggingAdapter = akka.event.Logging(this)
|
||||
|
||||
def mdc(currentMessage: Any): MDC = emptyMDC
|
||||
|
||||
override def log: LoggingAdapter = diagLog
|
||||
|
||||
override def aroundReceive(receive: Actor.Receive, msg: Any): Unit = try {
|
||||
diagLog.mdc(mdc(msg))
|
||||
super.aroundReceive(receive, msg)
|
||||
} finally {
|
||||
diagLog.clearMDC()
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
|
||||
import java.util.BitSet
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 13/02/2017.
|
||||
*/
|
||||
object Features {
|
||||
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
|
||||
val INITIAL_ROUTING_SYNC_BIT_MANDATORY = 2
|
||||
val INITIAL_ROUTING_SYNC_BIT_OPTIONAL = 3
|
||||
|
||||
/**
|
||||
*
|
||||
* @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 = {
|
||||
// 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)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* A feature set is supported if all even bits are supported.
|
||||
* We just ignore unknown odd bits.
|
||||
*/
|
||||
def areSupported(features: BinaryData): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray))
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.util.concurrent.atomic.{AtomicLong, AtomicReference}
|
||||
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeratesPerByte, FeeratesPerKw}
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
object Globals {
|
||||
|
||||
/**
|
||||
* This counter holds the current blockchain height.
|
||||
* It is mainly used to calculate htlc expiries.
|
||||
* The value is read by all actors, hence it needs to be thread-safe.
|
||||
*/
|
||||
val blockCount = new AtomicLong(0)
|
||||
|
||||
/**
|
||||
* This holds the current feerates, in satoshi-per-bytes.
|
||||
* The value is read by all actors, hence it needs to be thread-safe.
|
||||
*/
|
||||
val feeratesPerByte = new AtomicReference[FeeratesPerByte](null)
|
||||
|
||||
/**
|
||||
* This holds the current feerates, in satoshi-per-kw.
|
||||
* The value is read by all actors, hence it needs to be thread-safe.
|
||||
*/
|
||||
val feeratesPerKw = new AtomicReference[FeeratesPerKw](null)
|
||||
}
|
||||
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.sql.DriverManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
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.db._
|
||||
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb, SqlitePreimagesDb}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/**
|
||||
* Created by PM on 26/02/2017.
|
||||
*/
|
||||
case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
|
||||
privateKey: PrivateKey,
|
||||
alias: String,
|
||||
color: (Byte, Byte, Byte),
|
||||
publicAddresses: List[InetSocketAddress],
|
||||
globalFeatures: BinaryData,
|
||||
localFeatures: BinaryData,
|
||||
dustLimitSatoshis: Long,
|
||||
maxHtlcValueInFlightMsat: UInt64,
|
||||
maxAcceptedHtlcs: Int,
|
||||
expiryDeltaBlocks: Int,
|
||||
htlcMinimumMsat: Int,
|
||||
delayBlocks: Int,
|
||||
minDepthBlocks: Int,
|
||||
smartfeeNBlocks: Int,
|
||||
feeBaseMsat: Int,
|
||||
feeProportionalMillionth: Int,
|
||||
reserveToFundingRatio: Double,
|
||||
maxReserveToFundingRatio: Double,
|
||||
channelsDb: ChannelsDb,
|
||||
peersDb: PeersDb,
|
||||
networkDb: NetworkDb,
|
||||
preimagesDb: PreimagesDb,
|
||||
routerBroadcastInterval: FiniteDuration,
|
||||
routerValidateInterval: FiniteDuration,
|
||||
pingInterval: FiniteDuration,
|
||||
maxFeerateMismatch: Double,
|
||||
updateFeeMinDiffRatio: Double,
|
||||
autoReconnect: Boolean,
|
||||
chainHash: BinaryData,
|
||||
channelFlags: Byte,
|
||||
channelExcludeDuration: FiniteDuration,
|
||||
watcherType: WatcherType)
|
||||
|
||||
object NodeParams {
|
||||
|
||||
sealed trait WatcherType
|
||||
|
||||
object BITCOIND extends WatcherType
|
||||
|
||||
object BITCOINJ extends WatcherType
|
||||
|
||||
object ELECTRUM extends WatcherType
|
||||
|
||||
/**
|
||||
* Order of precedence for the configuration parameters:
|
||||
* 1) Java environment variables (-D...)
|
||||
* 2) Configuration file eclair.conf
|
||||
* 3) Optionally provided config
|
||||
* 4) Default values in reference.conf
|
||||
*/
|
||||
def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) =
|
||||
ConfigFactory.parseProperties(System.getProperties)
|
||||
.withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf")))
|
||||
.withFallback(overrideDefaults)
|
||||
.withFallback(ConfigFactory.load()).getConfig("eclair")
|
||||
|
||||
def makeNodeParams(datadir: File, config: Config): NodeParams = {
|
||||
|
||||
datadir.mkdirs()
|
||||
|
||||
val seedPath = new File(datadir, "seed.dat")
|
||||
val seed: BinaryData = seedPath.exists() match {
|
||||
case true => Files.readAllBytes(seedPath.toPath)
|
||||
case false =>
|
||||
val seed = randomKey.toBin
|
||||
Files.write(seedPath.toPath, seed)
|
||||
seed
|
||||
}
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
val extendedPrivateKey = DeterministicWallet.derivePrivateKey(master, DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil)
|
||||
|
||||
val chain = config.getString("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 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
|
||||
}
|
||||
|
||||
NodeParams(
|
||||
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 = config.getInt("max-accepted-htlcs"),
|
||||
expiryDeltaBlocks = config.getInt("expiry-delta-blocks"),
|
||||
htlcMinimumMsat = config.getInt("htlc-minimum-msat"),
|
||||
delayBlocks = config.getInt("delay-blocks"),
|
||||
minDepthBlocks = config.getInt("mindepth-blocks"),
|
||||
smartfeeNBlocks = 3,
|
||||
feeBaseMsat = config.getInt("fee-base-msat"),
|
||||
feeProportionalMillionth = config.getInt("fee-proportional-millionth"),
|
||||
reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"),
|
||||
maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"),
|
||||
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),
|
||||
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,
|
||||
channelExcludeDuration = FiniteDuration(config.getDuration("channel-exclude-duration").getSeconds, TimeUnit.SECONDS),
|
||||
watcherType = watcherType)
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.net.{InetAddress, ServerSocket}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object PortChecker {
|
||||
|
||||
/**
|
||||
* Tests if a port is open
|
||||
* See https://stackoverflow.com/questions/434718/sockets-discover-port-availability-using-java#435579
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def checkAvailable(host: String, port: Int): Unit = {
|
||||
Try(new ServerSocket(port, 50, InetAddress.getByName(host))) match {
|
||||
case Success(socket) =>
|
||||
Try(socket.close())
|
||||
case Failure(_) =>
|
||||
throw TCPBindException(port)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case class TCPBindException(port: Int) extends RuntimeException
|
||||
@ -1,215 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
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.typesafe.config.{Config, ConfigFactory}
|
||||
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.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.io.{Server, Switchboard}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router._
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
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}")
|
||||
|
||||
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()
|
||||
|
||||
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 ExtendedBitcoinClient(new BitcoinJsonRPCClient(
|
||||
user = config.getString("bitcoind.rpcuser"),
|
||||
password = config.getString("bitcoind.rpcpassword"),
|
||||
host = config.getString("bitcoind.host"),
|
||||
port = config.getInt("bitcoind.rpcport")))
|
||||
val future = for {
|
||||
json <- bitcoinClient.rpcClient.invoke("getblockchaininfo").recover { case _ => throw BitcoinRPCConnectionException }
|
||||
progress = (json \ "verificationprogress").extract[Double]
|
||||
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) = Await.result(future, 10 seconds)
|
||||
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
|
||||
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 =>
|
||||
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
|
||||
val addressesFile = chain match {
|
||||
case "test" => "/electrum/servers_testnet.json"
|
||||
case "regtest" => "/electrum/servers_regtest.json"
|
||||
}
|
||||
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.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
|
||||
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
|
||||
} yield kit
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// @formatter:off
|
||||
sealed trait Bitcoin
|
||||
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
|
||||
case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin
|
||||
case class Electrum(electrumClient: ActorRef) extends Bitcoin
|
||||
// @formatter:on
|
||||
|
||||
case class Kit(nodeParams: NodeParams,
|
||||
system: ActorSystem,
|
||||
watcher: ActorRef,
|
||||
paymentHandler: ActorRef,
|
||||
register: ActorRef,
|
||||
relayer: ActorRef,
|
||||
router: ActorRef,
|
||||
switchboard: ActorRef,
|
||||
paymentInitiator: ActorRef,
|
||||
server: ActorRef,
|
||||
wallet: EclairWallet)
|
||||
|
||||
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")
|
||||
@ -1,35 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
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)
|
||||
|
||||
override def toString: String = underlying.toString
|
||||
}
|
||||
|
||||
|
||||
object UInt64 {
|
||||
|
||||
private val MaxValueBigInt = BigInt(new BigInteger("ffffffffffffffff", 16))
|
||||
|
||||
val MaxValue = UInt64(MaxValueBigInt)
|
||||
|
||||
def apply(bin: BinaryData) = new UInt64(new BigInteger(1, bin))
|
||||
|
||||
def apply(value: Long) = new UInt64(BigInt(value))
|
||||
|
||||
object Conversions {
|
||||
|
||||
implicit def intToUint64(l: Int) = UInt64(l)
|
||||
|
||||
implicit def longToUint64(l: Long) = UInt64(l)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
package fr.acinq.eclair.api
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
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.HttpOriginRange.*
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.pattern.ask
|
||||
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.{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.JsonAST.{JInt, JString}
|
||||
import org.json4s.{JValue, jackson}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
|
||||
// @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
|
||||
|
||||
trait Service extends Logging {
|
||||
|
||||
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
|
||||
|
||||
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
|
||||
|
||||
import Json4sSupport.{marshaller, unmarshaller}
|
||||
|
||||
def appKit: Kit
|
||||
|
||||
def getInfoResponse: Future[GetInfoResponse]
|
||||
|
||||
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
|
||||
|
||||
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 route =
|
||||
respondWithDefaultHeaders(customHeaders) {
|
||||
pathSingleSlash {
|
||||
post {
|
||||
entity(as[JsonRPCBody]) {
|
||||
req =>
|
||||
val kit = appKit
|
||||
import kit._
|
||||
val f_res: Future[AnyRef] = req match {
|
||||
case JsonRPCBody(_, _, "getinfo", _) => getInfoResponse
|
||||
case JsonRPCBody(_, _, "connect", JString(nodeId) :: JString(host) :: JInt(port) :: Nil) =>
|
||||
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), None)).mapTo[String]
|
||||
case JsonRPCBody(_, _, "open", JString(nodeId) :: JString(host) :: JInt(port) :: JInt(fundingSatoshi) :: JInt(pushMsat) :: options) =>
|
||||
val channelFlags = options match {
|
||||
case JInt(value) :: Nil => Some(value.toByte)
|
||||
case _ => None // TODO: too lax?
|
||||
}
|
||||
(switchboard ? NewConnection(PublicKey(nodeId), new InetSocketAddress(host, port.toInt), Some(NewChannel(Satoshi(fundingSatoshi.toLong), MilliSatoshi(pushMsat.toLong), channelFlags)))).mapTo[String]
|
||||
case JsonRPCBody(_, _, "peers", _) =>
|
||||
(switchboard ? 'peers).mapTo[Map[PublicKey, ActorRef]].map(_.map(_._1.toBin))
|
||||
case JsonRPCBody(_, _, "channels", _) =>
|
||||
(register ? 'channels).mapTo[Map[Long, ActorRef]].map(_.keys)
|
||||
case JsonRPCBody(_, _, "channel", JString(channelId) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_GETINFO).mapTo[RES_GETINFO]
|
||||
case JsonRPCBody(_, _, "allnodes", _) =>
|
||||
(router ? 'nodes).mapTo[Iterable[NodeAnnouncement]].map(_.map(_.nodeId))
|
||||
case JsonRPCBody(_, _, "allchannels", _) =>
|
||||
(router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelInfo(c.shortChannelId.toHexString, c.nodeId1, c.nodeId2)))
|
||||
case JsonRPCBody(_, _, "receive", JInt(amountMsat) :: JString(description) :: Nil) =>
|
||||
(paymentHandler ? ReceivePayment(MilliSatoshi(amountMsat.toLong), description)).mapTo[PaymentRequest].map(PaymentRequest.write)
|
||||
case JsonRPCBody(_, _, "send", JInt(amountMsat) :: JString(paymentHash) :: JString(nodeId) :: Nil) =>
|
||||
(paymentInitiator ? SendPayment(amountMsat.toLong, paymentHash, PublicKey(nodeId))).mapTo[PaymentResult]
|
||||
case JsonRPCBody(_, _, "send", JString(paymentRequest) :: rest) =>
|
||||
for {
|
||||
req <- Future(PaymentRequest.read(paymentRequest))
|
||||
amount = (req.amount, rest) match {
|
||||
case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided
|
||||
case (Some(amt), _) => amt.amount
|
||||
case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one
|
||||
case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request")
|
||||
}
|
||||
sendPayment = req.minFinalCltvExpiry match {
|
||||
case None => SendPayment(amount, req.paymentHash, req.nodeId)
|
||||
case Some(value) => SendPayment(amount, req.paymentHash, req.nodeId, value)
|
||||
}
|
||||
res <- (paymentInitiator ? sendPayment).mapTo[PaymentResult]
|
||||
} yield res
|
||||
case JsonRPCBody(_, _, "close", JString(channelId) :: JString(scriptPubKey) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = Some(scriptPubKey))).mapTo[String]
|
||||
case JsonRPCBody(_, _, "close", JString(channelId) :: Nil) =>
|
||||
getChannel(channelId).flatMap(_ ? CMD_CLOSE(scriptPubKey = None)).mapTo[String]
|
||||
case JsonRPCBody(_, _, "help", _) =>
|
||||
Future.successful(List(
|
||||
"connect (nodeId, host, port): connect to another lightning node through a secure connection",
|
||||
"open (nodeId, host, port, fundingSatoshi, pushMsat, channelFlags = 0x01): open a channel with another lightning node",
|
||||
"peers: list existing local peers",
|
||||
"channels: list existing local channels",
|
||||
"channel (channelId): retrieve detailed information about a given channel",
|
||||
"allnodes: list all known nodes",
|
||||
"allchannels: list all known channels",
|
||||
"receive (amountMsat, description): generate a payment request for a given amount",
|
||||
"send (amountMsat, paymentHash, nodeId): send a payment to a lightning node",
|
||||
"send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request",
|
||||
"send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount",
|
||||
"close (channelId): close a channel",
|
||||
"close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey",
|
||||
"help: display this message"))
|
||||
case _ => Future.failed(new RuntimeException("method not found"))
|
||||
}
|
||||
|
||||
onComplete(f_res) {
|
||||
case Success(res) => complete(JsonRPCRes(res, None, req.id))
|
||||
case Failure(t) => complete(StatusCodes.InternalServerError, JsonRPCRes(null, Some(Error(-1, t.getMessage)), req.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* Created by PM on 06/07/2017.
|
||||
*/
|
||||
trait EclairWallet {
|
||||
|
||||
def getBalance: Future[Satoshi]
|
||||
|
||||
def getFinalAddress: Future[String]
|
||||
|
||||
def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]
|
||||
|
||||
/**
|
||||
* Committing *must* include publishing the transaction on the network.
|
||||
*
|
||||
* We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the
|
||||
* funding tx won't end up on the blockchain: if that happens and we have cancelled the channel, then we would lose our
|
||||
* funds!
|
||||
*
|
||||
* @param tx
|
||||
* @return true if success
|
||||
* false IF AND ONLY IF *HAS NOT BEEN PUBLISHED* otherwise funds are at risk!!!
|
||||
*/
|
||||
def commit(tx: Transaction): Future[Boolean]
|
||||
|
||||
/**
|
||||
* Cancels this transaction: this probably translates to "release locks on utxos".
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
def rollback(tx: Transaction): Future[Boolean]
|
||||
|
||||
}
|
||||
|
||||
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)
|
||||
@ -1,67 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Script, ScriptWitness, Transaction}
|
||||
import fr.acinq.eclair.channel.BitcoinEvent
|
||||
import fr.acinq.eclair.wire.ChannelAnnouncement
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by PM on 19/01/2016.
|
||||
*/
|
||||
|
||||
// @formatter:off
|
||||
|
||||
sealed trait Watch {
|
||||
def channel: ActorRef
|
||||
def event: BitcoinEvent
|
||||
}
|
||||
// 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, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
|
||||
|
||||
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
|
||||
witness.stack.last
|
||||
}
|
||||
}
|
||||
|
||||
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: 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: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
|
||||
trait WatchEvent {
|
||||
def event: BitcoinEvent
|
||||
}
|
||||
final case class WatchEventConfirmed(event: BitcoinEvent, blockHeight: Int, txIndex: Int) extends WatchEvent
|
||||
final case class WatchEventSpent(event: BitcoinEvent, tx: Transaction) extends WatchEvent
|
||||
final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent
|
||||
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 ParallelGetRequest(ann: Seq[ChannelAnnouncement])
|
||||
final case class IndividualResult(c: ChannelAnnouncement, tx: Option[Transaction], unspent: Boolean)
|
||||
final case class ParallelGetResponse(r: Seq[IndividualResult])
|
||||
|
||||
// @formatter:on
|
||||
@ -1,214 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoind
|
||||
|
||||
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, 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.JsonAST.{JBool, JDouble, JInt, JString}
|
||||
|
||||
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, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
override def getBalance: Future[Satoshi] = ???
|
||||
|
||||
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 JDouble(fee) = json \ "fee"
|
||||
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
|
||||
})
|
||||
}
|
||||
|
||||
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
|
||||
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
|
||||
|
||||
def signTransaction(hex: String): Future[SignTransactionResponse] =
|
||||
rpcClient.invoke("signrawtransaction", hex).map(json => {
|
||||
val JString(hex) = json \ "hex"
|
||||
val JBool(complete) = json \ "complete"
|
||||
SignTransactionResponse(Transaction.read(hex), complete)
|
||||
})
|
||||
|
||||
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
|
||||
signTransaction(Transaction.write(tx).toString())
|
||||
|
||||
def getTransaction(txid: BinaryData): Future[Transaction] = {
|
||||
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
|
||||
val JString(hex) = json
|
||||
Transaction.read(hex)
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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 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
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
case class Options(lockUnspents: Boolean)
|
||||
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,193 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props, Terminated}
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Script.{pay2wsh, write}
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.{Globals, fromShortId}
|
||||
import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.script.Script
|
||||
|
||||
import scala.collection.SortedMap
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
final case class NewConfidenceLevel(tx: Transaction, blockHeight: Int, confirmations: Int) extends BlockchainEvent
|
||||
|
||||
/**
|
||||
* A blockchain watcher that:
|
||||
* - receives bitcoin events (new blocks and new txes) directly from the bitcoin network
|
||||
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
|
||||
* Created by PM on 21/02/2016.
|
||||
*/
|
||||
class BitcoinjWatcher(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel])
|
||||
|
||||
val broadcaster = context.actorOf(Props(new Broadcaster(kit: WalletAppKit)), name = "broadcaster")
|
||||
|
||||
case class TriggerEvent(w: Watch, e: WatchEvent)
|
||||
|
||||
def receive: Receive = watching(Set(), SortedMap(), Nil, Nil)
|
||||
|
||||
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = {
|
||||
|
||||
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
|
||||
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=${Transaction.write(tx)}")
|
||||
watches.collect {
|
||||
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(event))
|
||||
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpent(event, tx))
|
||||
case w@WatchConfirmed(_, txId, _, minDepth, event) if txId == tx.txid && confirmations >= minDepth =>
|
||||
self ! TriggerEvent(w, WatchEventConfirmed(event, blockHeight, 0))
|
||||
}
|
||||
context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event, sent)
|
||||
|
||||
case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) =>
|
||||
log.info(s"triggering $w")
|
||||
w.channel ! e
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches
|
||||
context.become(watching(newWatches, block2tx, oldEvents, sent :+ t))
|
||||
|
||||
case CurrentBlockCount(count) => {
|
||||
val toPublish = block2tx.filterKeys(_ <= count)
|
||||
toPublish.values.flatten.map(tx => publish(tx))
|
||||
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent))
|
||||
}
|
||||
|
||||
case w: Watch if !watches.contains(w) =>
|
||||
w match {
|
||||
case w: WatchConfirmed => addHint(w.publicKeyScript)
|
||||
case w: WatchSpent => addHint(w.publicKeyScript)
|
||||
case w: WatchSpentBasic => addHint(w.publicKeyScript)
|
||||
case _ => ()
|
||||
}
|
||||
log.debug(s"adding watch $w for $sender")
|
||||
log.info(s"resending ${oldEvents.size} events!")
|
||||
oldEvents.foreach(self ! _)
|
||||
context.watch(w.channel)
|
||||
context.become(watching(watches + w, block2tx, oldEvents, sent))
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val cltvTimeout = Scripts.cltvTimeout(tx)
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${Transaction.write(tx)}")
|
||||
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
|
||||
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
|
||||
} else if (cltvTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
|
||||
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
val absTimeout = blockHeight + csvTimeout
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
|
||||
case c =>
|
||||
log.info(s"blindly validating channel=$c")
|
||||
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
|
||||
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
|
||||
val fakeFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format
|
||||
lockTime = 0)
|
||||
IndividualResult(c, Some(fakeFundingTx), true)
|
||||
})
|
||||
|
||||
case Terminated(channel) =>
|
||||
// we remove watches associated to dead actor
|
||||
val deprecatedWatches = watches.filter(_.channel == channel)
|
||||
context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents, sent))
|
||||
|
||||
case 'watches => sender ! watches
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitcoinj needs hints to be able to detect transactions
|
||||
*
|
||||
* @param pubkeyScript
|
||||
* @return
|
||||
*/
|
||||
def addHint(pubkeyScript: BinaryData) = {
|
||||
Context.propagate(kit.wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
// set creation time to 2017/09/01, so bitcoinj can still use its checkpoints optimizations
|
||||
script.setCreationTimeSeconds(1501538400L) // 2017-09-01
|
||||
kit.wallet().addWatchedScripts(ImmutableList.of(script))
|
||||
}
|
||||
|
||||
def publish(tx: Transaction): Unit = broadcaster ! tx
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjWatcher {
|
||||
|
||||
def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new BitcoinjWatcher(kit)(ec))
|
||||
|
||||
}
|
||||
|
||||
class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging {
|
||||
|
||||
override def receive: Receive = {
|
||||
case tx: Transaction =>
|
||||
broadcast(tx)
|
||||
context become waiting(Nil)
|
||||
}
|
||||
|
||||
def waiting(stash: Seq[Transaction]): Receive = {
|
||||
case BroadcastResult(tx, result) =>
|
||||
result match {
|
||||
case Success(_) => log.info(s"broadcast success for txid=${tx.txid}")
|
||||
case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ")
|
||||
}
|
||||
stash match {
|
||||
case head :: rest =>
|
||||
broadcast(head)
|
||||
context become waiting(rest)
|
||||
case Nil => context become receive
|
||||
}
|
||||
case tx: Transaction =>
|
||||
log.info(s"stashing txid=${tx.txid} for broadcast")
|
||||
context become waiting(stash :+ tx)
|
||||
}
|
||||
|
||||
case class BroadcastResult(tx: Transaction, result: Try[Boolean])
|
||||
|
||||
def broadcast(tx: Transaction) = {
|
||||
Context.propagate(kit.wallet().getContext)
|
||||
val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx))
|
||||
log.info(s"broadcasting txid=${tx.txid}")
|
||||
Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] {
|
||||
override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t))
|
||||
|
||||
override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true))
|
||||
}, context.dispatcher)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,488 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
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 org.json4s.JsonAST._
|
||||
import org.json4s.jackson.JsonMethods
|
||||
import org.json4s.{DefaultFormats, JInt, JLong, JString}
|
||||
import org.spongycastle.util.encoders.Hex
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
class ElectrumClient(serverAddresses: Seq[InetSocketAddress]) extends Actor with Stash with ActorLogging {
|
||||
|
||||
import ElectrumClient._
|
||||
import context.system
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
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
|
||||
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) =>
|
||||
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 _: ServerVersion => () // we only handle this when connected
|
||||
|
||||
case _: ServerVersionResponse => () // we just ignore these messages, they are used as pings
|
||||
|
||||
case _ => log.warning(s"unhandled $message")
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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 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 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 connected(connection: ActorRef, remoteAddress: InetSocketAddress, tip: Header, buffer: String, requests: Map[String, (Request, ActorRef)]): Receive = {
|
||||
case AddStatusListener(actor) =>
|
||||
statusListeners += actor
|
||||
actor ! ElectrumReady
|
||||
|
||||
case HeaderSubscription(actor) =>
|
||||
headerSubscriptions += actor
|
||||
actor ! HeaderSubscriptionResponse(tip)
|
||||
context watch actor
|
||||
|
||||
case request: Request =>
|
||||
val curReqId = "" + reqId
|
||||
send(connection, makeRequest(request, curReqId))
|
||||
request match {
|
||||
case AddressSubscription(address, actor) =>
|
||||
addressSubscriptions.update(address, addressSubscriptions.getOrElse(address, Set()) + actor)
|
||||
context watch actor
|
||||
case ScriptHashSubscription(scriptHash, actor) =>
|
||||
scriptHashSubscriptions.update(scriptHash, scriptHashSubscriptions.getOrElse(scriptHash, Set()) + actor)
|
||||
context watch actor
|
||||
case _ => ()
|
||||
}
|
||||
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(s"got response for reqId=${json.id} request=$request response=$response")
|
||||
requestor ! response
|
||||
case None =>
|
||||
log.warning(s"could not find requestor for reqId=${json.id} response=$json")
|
||||
}
|
||||
context become connected(connection, remoteAddress, tip, buffer, requests - json.id)
|
||||
|
||||
case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.map(_ ! response)
|
||||
|
||||
case Left(response: AddressSubscriptionResponse) => addressSubscriptions.get(response.address).map(listeners => listeners.map(_ ! response))
|
||||
|
||||
case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).map(listeners => listeners.map(_ ! response))
|
||||
|
||||
case HeaderSubscriptionResponse(newtip) =>
|
||||
log.info(s"new tip $newtip")
|
||||
updateBlockCount(newtip.block_height)
|
||||
context become connected(connection, remoteAddress, newtip, buffer, requests)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ElectrumClient {
|
||||
|
||||
def apply(addresses: java.util.List[InetSocketAddress]): ElectrumClient = {
|
||||
import collection.JavaConversions._
|
||||
new ElectrumClient(addresses)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to converts a publicKeyScript to electrum's scripthash
|
||||
*
|
||||
* @param publicKeyScript public key script
|
||||
* @return the hash of the public key script, as used by ElectrumX's hash-based methods
|
||||
*/
|
||||
def computeScriptHash(publicKeyScript: BinaryData): BinaryData = Crypto.sha256(publicKeyScript).reverse
|
||||
|
||||
// @formatter:off
|
||||
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 class GetAddressHistory(address: String) extends Request
|
||||
case class TransactionHistoryItem(height: Long, tx_hash: BinaryData)
|
||||
case class GetAddressHistoryResponse(address: String, history: Seq[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: 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: 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: BinaryData) extends Request
|
||||
case class GetTransactionResponse(tx: Transaction) extends Response
|
||||
|
||||
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[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, 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: BinaryData, actor: ActorRef) extends Request
|
||||
case class ScriptHashSubscriptionResponse(scriptHash: BinaryData, status: String) extends Response
|
||||
|
||||
case class HeaderSubscription(actor: ActorRef) extends Request
|
||||
case class HeaderSubscriptionResponse(header: Header) extends Response
|
||||
|
||||
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(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)
|
||||
}
|
||||
|
||||
case class TransactionHistory(history: Seq[TransactionHistoryItem]) extends Response
|
||||
|
||||
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 object ElectrumConnected extends ElectrumEvent
|
||||
case object ElectrumReady extends ElectrumEvent
|
||||
case object ElectrumDisconnected extends ElectrumEvent
|
||||
|
||||
// @formatter:on
|
||||
|
||||
def parseResponse(input: String): Either[Response, JsonRPCResponse] = {
|
||||
implicit val formats = DefaultFormats
|
||||
val json = JsonMethods.parse(new String(input))
|
||||
json \ "method" match {
|
||||
case JString(method) =>
|
||||
// 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(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(BinaryData(scriptHashHex), "")
|
||||
case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JString(status) :: Nil) => ScriptHashSubscriptionResponse(BinaryData(scriptHashHex), status)
|
||||
})
|
||||
case _ => Right(parseJsonRpcResponse(json))
|
||||
}
|
||||
}
|
||||
|
||||
def parseJsonRpcResponse(json: JValue): JsonRPCResponse = {
|
||||
implicit val formats = DefaultFormats
|
||||
val result = json \ "result"
|
||||
val error = json \ "error" match {
|
||||
case JNull => None
|
||||
case JNothing => None
|
||||
case other =>
|
||||
val message = other \ "message" match {
|
||||
case JString(value) => value
|
||||
case _ => ""
|
||||
}
|
||||
val code = other \ " code" match {
|
||||
case JInt(value) => value.intValue()
|
||||
case JLong(value) => value.intValue()
|
||||
case _ => 0
|
||||
}
|
||||
Some(Error(code, message))
|
||||
}
|
||||
val id = json \ "id" match {
|
||||
case JString(value) => value
|
||||
case JInt(value) => value.toString()
|
||||
case JLong(value) => value.toString
|
||||
case _ => ""
|
||||
}
|
||||
JsonRPCResponse(result, error, id)
|
||||
}
|
||||
|
||||
def longField(jvalue: JValue, field: String): Long = (jvalue \ field: @unchecked) match {
|
||||
case JLong(value) => value.longValue()
|
||||
case JInt(value) => value.longValue()
|
||||
}
|
||||
|
||||
def intField(jvalue: JValue, field: String): Int = (jvalue \ field: @unchecked) match {
|
||||
case JLong(value) => value.intValue()
|
||||
case JInt(value) => value.intValue()
|
||||
}
|
||||
|
||||
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 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.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.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 = 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 GetMerkle(txid, height) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get_merkle", params = txid :: height :: Nil)
|
||||
}
|
||||
|
||||
def parseJsonResponse(request: Request, json: JsonRPCResponse): Response = {
|
||||
implicit val formats = DefaultFormats
|
||||
json.error match {
|
||||
case Some(error) => (request: @unchecked) match {
|
||||
case BroadcastTransaction(tx) => BroadcastTransactionResponse(tx, Some(error)) // for this request type, error are considered a "normal" response
|
||||
case _ => ServerError(request, error)
|
||||
}
|
||||
case None => (request: @unchecked) match {
|
||||
case s: ServerVersion =>
|
||||
val JArray(jitems) = json.result
|
||||
val JString(clientName) = jitems(0)
|
||||
val JString(protocolVersion) = jitems(1)
|
||||
ServerVersionResponse(clientName, protocolVersion)
|
||||
case GetAddressHistory(address) =>
|
||||
val JArray(jitems) = json.result
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "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 = longField(jvalue, "height")
|
||||
TransactionHistoryItem(height, tx_hash)
|
||||
})
|
||||
GetScriptHashHistoryResponse(scripthash, items)
|
||||
case AddressListUnspent(address) =>
|
||||
val JArray(jitems) = json.result
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "tx_hash"
|
||||
val tx_pos = intField(jvalue, "tx_pos")
|
||||
val height = longField(jvalue, "height")
|
||||
val value = longField(jvalue, "value")
|
||||
UnspentItem(tx_hash, tx_pos, value, height)
|
||||
})
|
||||
AddressListUnspentResponse(address, items)
|
||||
case ScriptHashListUnspent(scripthash) =>
|
||||
val JArray(jitems) = json.result
|
||||
val items = jitems.map(jvalue => {
|
||||
val JString(tx_hash) = jvalue \ "tx_hash"
|
||||
val tx_pos = intField(jvalue, "tx_pos")
|
||||
val height = longField(jvalue, "height")
|
||||
val value = longField(jvalue, "value")
|
||||
UnspentItem(tx_hash, tx_pos, value, height)
|
||||
})
|
||||
ScriptHashListUnspentResponse(scripthash, items)
|
||||
case GetTransaction(_) =>
|
||||
val JString(hex) = json.result
|
||||
GetTransactionResponse(Transaction.read(hex))
|
||||
case AddressSubscription(address, _) => json.result match {
|
||||
case JString(status) => AddressSubscriptionResponse(address, status)
|
||||
case _ => AddressSubscriptionResponse(address, "")
|
||||
}
|
||||
case ScriptHashSubscription(scriptHash, _) => json.result match {
|
||||
case JString(status) => ScriptHashSubscriptionResponse(scriptHash, status)
|
||||
case _ => ScriptHashSubscriptionResponse(scriptHash, "")
|
||||
}
|
||||
case BroadcastTransaction(tx) =>
|
||||
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) => BinaryData(value) }
|
||||
val blockHeight = longField(json.result, "block_height")
|
||||
val JInt(pos) = json.result \ "pos"
|
||||
GetMerkleResponse(txid, leaves, blockHeight, pos.toInt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def readServerAddresses(stream: InputStream): Seq[InetSocketAddress] = try {
|
||||
val JObject(values) = JsonMethods.parse(stream)
|
||||
val addresses = values.map {
|
||||
case (name, fields) =>
|
||||
val JString(port) = fields \ "t"
|
||||
new InetSocketAddress(name, port.toInt)
|
||||
}
|
||||
val randomized = Random.shuffle(addresses)
|
||||
randomized
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.pattern.ask
|
||||
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 scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
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)
|
||||
|
||||
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, None) => MakeFundingTxResponse(tx1, 0)
|
||||
case CompleteTransactionResponse(_, Some(error)) => throw error
|
||||
})
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] =
|
||||
(wallet ? BroadcastTransaction(tx)) flatMap {
|
||||
case ElectrumClient.BroadcastTransactionResponse(tx, None) =>
|
||||
//tx broadcast successfully: commit tx
|
||||
wallet ? CommitTransaction(tx)
|
||||
case ElectrumClient.BroadcastTransactionResponse(tx, Some(error)) if error.message.contains("transaction already in block chain") =>
|
||||
// tx was already in the blockchain, that's weird but it is OK
|
||||
wallet ? CommitTransaction(tx)
|
||||
case ElectrumClient.BroadcastTransactionResponse(_, Some(error)) =>
|
||||
//tx broadcast failed: cancel tx
|
||||
logger.error(s"cannot broadcast tx ${tx.txid}: $error")
|
||||
wallet ? CancelTransaction(tx)
|
||||
case ElectrumClient.ServerError(ElectrumClient.BroadcastTransaction(tx), error) =>
|
||||
//tx broadcast failed: cancel tx
|
||||
logger.error(s"cannot broadcast tx ${tx.txid}: $error")
|
||||
wallet ? CancelTransaction(tx)
|
||||
} map {
|
||||
case CommitTransactionResponse(_) => true
|
||||
case CancelTransactionResponse(_) => false
|
||||
}
|
||||
|
||||
def sendPayment(amount: Satoshi, address: String, feeRatePerKw: Long): Future[String] = {
|
||||
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 true => tx.txid.toString()
|
||||
case false => throw new RuntimeException(s"could not commit tx=${Transaction.write(tx)}")
|
||||
}
|
||||
case CompleteTransactionResponse(_, Some(error)) => throw error
|
||||
}
|
||||
}
|
||||
|
||||
def getMnemonics: Future[Seq[String]] = (wallet ? GetMnemonicCode).mapTo[GetMnemonicCodeResponse].map(_.mnemonics)
|
||||
|
||||
override def rollback(tx: Transaction): Future[Boolean] = (wallet ? CancelTransaction(tx)).map(_ => true)
|
||||
}
|
||||
@ -1,693 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import java.io.File
|
||||
|
||||
import akka.actor.{ActorRef, LoggingFSM, Props}
|
||||
import com.google.common.io.Files
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey, hardened}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, DeterministicWallet, MnemonicCode, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.Error
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{GetTransaction, GetTransactionResponse, TransactionHistoryItem, computeScriptHash}
|
||||
import fr.acinq.eclair.randomBytes
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Simple electrum wallet
|
||||
*
|
||||
* Typical workflow:
|
||||
*
|
||||
* client ---- header update ----> wallet
|
||||
* client ---- status update ----> wallet
|
||||
* client <--- ask history ----- wallet
|
||||
* client ---- history ----> wallet
|
||||
* client <--- ask tx ----- wallet
|
||||
* client ---- tx ----> wallet
|
||||
*
|
||||
* @param mnemonics
|
||||
* @param client
|
||||
* @param params
|
||||
*/
|
||||
class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumWallet.WalletParameters) extends LoggingFSM[ElectrumWallet.State, ElectrumWallet.Data] {
|
||||
|
||||
import ElectrumWallet._
|
||||
import params._
|
||||
|
||||
val seed = MnemonicCode.toSeed(mnemonics, "")
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
|
||||
val accountMaster = accountKey(master)
|
||||
val changeMaster = changeKey(master)
|
||||
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
|
||||
// disconnected --> waitingForTip --> running --
|
||||
// ^ |
|
||||
// | |
|
||||
// --------------------------------------------
|
||||
|
||||
startWith(DISCONNECTED, {
|
||||
val header = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash => ElectrumClient.Header.RegtestGenesisHeader
|
||||
case Block.TestnetGenesisBlock.hash => ElectrumClient.Header.TestnetGenesisHeader
|
||||
}
|
||||
val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
|
||||
val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
|
||||
val data = Data(params, header, firstAccountKeys, firstChangeKeys)
|
||||
context.system.eventStream.publish(NewWalletReceiveAddress(data.currentReceiveAddress))
|
||||
data
|
||||
})
|
||||
|
||||
when(DISCONNECTED) {
|
||||
case Event(ElectrumClient.ElectrumReady, data) =>
|
||||
client ! ElectrumClient.HeaderSubscription(self)
|
||||
goto(WAITING_FOR_TIP) using data
|
||||
}
|
||||
|
||||
when(WAITING_FOR_TIP) {
|
||||
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) =>
|
||||
data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
|
||||
data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
|
||||
goto(RUNNING) using data.copy(tip = header)
|
||||
|
||||
case Event(ElectrumClient.ElectrumDisconnected, data) =>
|
||||
log.info(s"wallet got disconnected")
|
||||
goto(DISCONNECTED) using data
|
||||
}
|
||||
|
||||
when(RUNNING) {
|
||||
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) if data.tip == header => stay
|
||||
|
||||
case Event(ElectrumClient.HeaderSubscriptionResponse(header), data) =>
|
||||
log.info(s"got new tip ${header.block_hash} at ${header.block_height}")
|
||||
data.heights.collect {
|
||||
case (txid, height) if height > 0 =>
|
||||
val confirmations = computeDepth(header.block_height, height)
|
||||
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
|
||||
}
|
||||
stay using data.copy(tip = header)
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if data.status.get(scriptHash) == Some(status) => stay // we already have it
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if !data.accountKeyMap.contains(scriptHash) && !data.changeKeyMap.contains(scriptHash) =>
|
||||
log.warning(s"received status=$status for scriptHash=$scriptHash which does not match any of our keys")
|
||||
stay
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if status == "" =>
|
||||
val data1 = data.copy(status = data.status + (scriptHash -> status)) // empty status, nothing to do
|
||||
goto(stateName) using data1
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) =>
|
||||
val key = data.accountKeyMap.getOrElse(scriptHash, data.changeKeyMap(scriptHash))
|
||||
val isChange = data.changeKeyMap.contains(scriptHash)
|
||||
log.info(s"received status=$status for scriptHash=$scriptHash key=${segwitAddress(key)} isChange=$isChange")
|
||||
|
||||
// let's retrieve the tx history for this key
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
|
||||
val (newAccountKeys, newChangeKeys) = data.status.get(status) match {
|
||||
case None =>
|
||||
// first time this script hash is used, need to generate a new key
|
||||
val newKey = if (isChange) derivePrivateKey(changeMaster, data.changeKeys.last.path.lastChildNumber + 1) else derivePrivateKey(accountMaster, data.accountKeys.last.path.lastChildNumber + 1)
|
||||
val newScriptHash = computeScriptHashFromPublicKey(newKey.publicKey)
|
||||
log.info(s"generated key with index=${newKey.path.lastChildNumber} scriptHash=$newScriptHash key=${segwitAddress(newKey)} isChange=$isChange")
|
||||
// listens to changes for the newly generated key
|
||||
client ! ElectrumClient.ScriptHashSubscription(newScriptHash, self)
|
||||
if (isChange) (data.accountKeys, data.changeKeys :+ newKey) else (data.accountKeys :+ newKey, data.changeKeys)
|
||||
case Some(_) => (data.accountKeys, data.changeKeys)
|
||||
}
|
||||
|
||||
val data1 = data.copy(
|
||||
accountKeys = newAccountKeys,
|
||||
changeKeys = newChangeKeys,
|
||||
status = data.status + (scriptHash -> status),
|
||||
pendingHistoryRequests = data.pendingHistoryRequests + scriptHash)
|
||||
|
||||
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
|
||||
|
||||
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, history), data) =>
|
||||
log.debug(s"scriptHash=$scriptHash has history=$history")
|
||||
val (heights1, pendingTransactionRequests1) = history.foldLeft((data.heights, data.pendingTransactionRequests)) {
|
||||
case ((heights, hashes), item) if !data.transactions.contains(item.tx_hash) && !data.pendingTransactionRequests.contains(item.tx_hash) =>
|
||||
// we retrieve the tx if we don't have it and haven't yet requested it
|
||||
client ! GetTransaction(item.tx_hash)
|
||||
(heights + (item.tx_hash -> item.height), hashes + item.tx_hash)
|
||||
case ((heights, hashes), item) =>
|
||||
// otherwise we just update the height
|
||||
(heights + (item.tx_hash -> item.height), hashes)
|
||||
}
|
||||
|
||||
// we now have updated height for all our transactions,
|
||||
heights1.collect {
|
||||
case (txid, height) =>
|
||||
val confirmations = if (height <= 0) 0 else computeDepth(data.tip.block_height, height)
|
||||
(data.heights.get(txid), height) match {
|
||||
case (None, height) if height <= 0 =>
|
||||
// height=0 => unconfirmed, height=-1 => unconfirmed and one input is unconfirmed
|
||||
case (None, height) if height > 0 =>
|
||||
// first time we get a height for this tx: either it was just confirmed, or we restarted the wallet
|
||||
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
|
||||
case (Some(previousHeight), height) if previousHeight != height =>
|
||||
// there was a reorg
|
||||
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations))
|
||||
case (Some(previousHeight), height) if previousHeight == height =>
|
||||
// no reorg, nothing to do
|
||||
}
|
||||
}
|
||||
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> history), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
|
||||
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
|
||||
|
||||
case Event(GetTransactionResponse(tx), data) =>
|
||||
log.debug(s"received transaction ${tx.txid}")
|
||||
data.computeTransactionDelta(tx) match {
|
||||
case Some((received, sent, fee_opt)) =>
|
||||
log.info(s"successfully connected txid=${tx.txid}")
|
||||
context.system.eventStream.publish(TransactionReceived(tx, data.computeTransactionDepth(tx.txid), received, sent, fee_opt))
|
||||
// when we have successfully processed a new tx, we retry all pending txes to see if they can be added now
|
||||
data.pendingTransactions.foreach(self ! GetTransactionResponse(_))
|
||||
val data1 = data.copy(transactions = data.transactions + (tx.txid -> tx), pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = Nil)
|
||||
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
|
||||
case None =>
|
||||
// missing parents
|
||||
log.info(s"couldn't connect txid=${tx.txid}")
|
||||
val data1 = data.copy(pendingTransactions = data.pendingTransactions :+ tx)
|
||||
stay using data1
|
||||
}
|
||||
|
||||
case Event(CompleteTransaction(tx, feeRatePerKw), data) =>
|
||||
Try(data.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, allowSpendUnconfirmed)) match {
|
||||
case Success((data1, tx1)) => stay using data1 replying CompleteTransactionResponse(tx1, None)
|
||||
case Failure(t) => stay replying CompleteTransactionResponse(tx, Some(t))
|
||||
}
|
||||
|
||||
case Event(CommitTransaction(tx), data) =>
|
||||
log.info(s"committing txid=${tx.txid}")
|
||||
val data1 = data.commitTransaction(tx)
|
||||
// we use the initial state to compute the effect of the tx
|
||||
// note: we know that computeTransactionDelta and the fee will be defined, because we built the tx ourselves so
|
||||
// we know all the parents
|
||||
val (received, sent, Some(fee)) = data.computeTransactionDelta(tx).get
|
||||
// we notify here because the tx won't be downloaded again (it has been added to the state at commit)
|
||||
context.system.eventStream.publish(TransactionReceived(tx, data1.computeTransactionDepth(tx.txid), received, sent, Some(fee)))
|
||||
goto(stateName) using data1 replying CommitTransactionResponse(tx) // goto instead of stay because we want to fire transitions
|
||||
|
||||
case Event(CancelTransaction(tx), data) =>
|
||||
log.info(s"cancelling txid=${tx.txid}")
|
||||
stay using data.cancelTransaction(tx) replying CancelTransactionResponse(tx)
|
||||
|
||||
case Event(bc@ElectrumClient.BroadcastTransaction(tx), _) =>
|
||||
log.info(s"broadcasting txid=${tx.txid}")
|
||||
client forward bc
|
||||
stay
|
||||
|
||||
case Event(ElectrumClient.ElectrumDisconnected, data) =>
|
||||
log.info(s"wallet got disconnected")
|
||||
goto(DISCONNECTED) using data
|
||||
}
|
||||
|
||||
whenUnhandled {
|
||||
case Event(GetMnemonicCode, _) => stay replying GetMnemonicCodeResponse(mnemonics)
|
||||
|
||||
case Event(GetCurrentReceiveAddress, data) => stay replying GetCurrentReceiveAddressResponse(data.currentReceiveAddress)
|
||||
|
||||
case Event(GetBalance, data) =>
|
||||
val (confirmed, unconfirmed) = data.balance
|
||||
stay replying GetBalanceResponse(confirmed, unconfirmed)
|
||||
|
||||
case Event(GetData, data) => stay replying GetDataResponse(data)
|
||||
|
||||
case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected")))
|
||||
}
|
||||
|
||||
onTransition {
|
||||
case _ -> _ if nextStateData.isReady(params.swipeRange) =>
|
||||
val ready = nextStateData.readyMessage
|
||||
log.info(s"wallet is ready with $ready")
|
||||
context.system.eventStream.publish(ready)
|
||||
context.system.eventStream.publish(NewWalletReceiveAddress(nextStateData.currentReceiveAddress))
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
||||
}
|
||||
|
||||
object ElectrumWallet {
|
||||
|
||||
// use 32 bytes seed, which will generate a 24 words mnemonic code
|
||||
val SEED_BYTES_LENGTH = 32
|
||||
|
||||
def props(mnemonics: Seq[String], client: ActorRef, params: WalletParameters): Props = Props(new ElectrumWallet(mnemonics, client, params))
|
||||
|
||||
def props(file: File, client: ActorRef, params: WalletParameters): Props = {
|
||||
val entropy: BinaryData = (file.exists(), file.canRead(), file.isFile) match {
|
||||
case (true, true, true) => Files.toByteArray(file)
|
||||
case (false, _, _) =>
|
||||
val buffer = randomBytes(SEED_BYTES_LENGTH)
|
||||
Files.write(buffer, file)
|
||||
buffer
|
||||
case _ => throw new IllegalArgumentException(s"cannot create wallet:$file exist but cannot read from")
|
||||
}
|
||||
val mnemonics = MnemonicCode.toMnemonics(entropy)
|
||||
Props(new ElectrumWallet(mnemonics, client, params))
|
||||
}
|
||||
|
||||
case class WalletParameters(chainHash: BinaryData, minimumFee: Satoshi = Satoshi(2000), dustLimit: Satoshi = Satoshi(546), swipeRange: Int = 10, allowSpendUnconfirmed: Boolean = true)
|
||||
|
||||
// @formatter:off
|
||||
sealed trait State
|
||||
case object DISCONNECTED extends State
|
||||
case object WAITING_FOR_TIP extends State
|
||||
case object RUNNING extends State
|
||||
|
||||
sealed trait Request
|
||||
sealed trait Response
|
||||
|
||||
case object GetMnemonicCode extends RuntimeException
|
||||
case class GetMnemonicCodeResponse(mnemonics: Seq[String]) extends Response
|
||||
|
||||
case object GetBalance extends Request
|
||||
case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response
|
||||
|
||||
case object GetCurrentReceiveAddress extends Request
|
||||
case class GetCurrentReceiveAddressResponse(address: String) extends Response
|
||||
|
||||
case object GetData extends Request
|
||||
case class GetDataResponse(state: Data) extends Response
|
||||
|
||||
case class CompleteTransaction(tx: Transaction, feeRatePerKw: Long) extends Request
|
||||
case class CompleteTransactionResponse(tx: Transaction, error: Option[Throwable]) extends Response
|
||||
|
||||
case class CommitTransaction(tx: Transaction) extends Request
|
||||
case class CommitTransactionResponse(tx: Transaction) extends Response
|
||||
|
||||
case class SendTransaction(tx: Transaction) extends Request
|
||||
case class SendTransactionReponse(tx: Transaction) extends Response
|
||||
|
||||
case class CancelTransaction(tx: Transaction) extends Request
|
||||
case class CancelTransactionResponse(tx: Transaction) extends Response
|
||||
|
||||
case object InsufficientFunds extends Response
|
||||
case class AmountBelowDustLimit(dustLimit: Satoshi) extends Response
|
||||
|
||||
case class GetPrivateKey(address: String) extends Request
|
||||
case class GetPrivateKeyResponse(address: String, key: Option[ExtendedPrivateKey]) extends Response
|
||||
|
||||
|
||||
sealed trait WalletEvent
|
||||
/**
|
||||
*
|
||||
* @param tx
|
||||
* @param depth
|
||||
* @param received
|
||||
* @param sent
|
||||
* @param feeOpt is set only when we know it (i.e. for outgoing transactions)
|
||||
*/
|
||||
case class TransactionReceived(tx: Transaction, depth: Long, received: Satoshi, sent: Satoshi, feeOpt: Option[Satoshi]) extends WalletEvent
|
||||
case class TransactionConfidenceChanged(txid: BinaryData, depth: Long) extends WalletEvent
|
||||
case class NewWalletReceiveAddress(address: String) extends WalletEvent
|
||||
case class WalletReady(confirmedBalance: Satoshi, unconfirmedBalance: Satoshi, height: Long) extends WalletEvent
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key public key
|
||||
* @return the address of the p2sh-of-p2wpkh script for this key
|
||||
*/
|
||||
def segwitAddress(key: PublicKey): String = {
|
||||
val script = Script.pay2wpkh(key)
|
||||
val hash = Crypto.hash160(Script.write(script))
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
|
||||
}
|
||||
|
||||
def segwitAddress(key: ExtendedPrivateKey): String = segwitAddress(key.publicKey)
|
||||
|
||||
def segwitAddress(key: PrivateKey): String = segwitAddress(key.publicKey)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key public key
|
||||
* @return a p2sh-of-p2wpkh script for this key
|
||||
*/
|
||||
def computePublicKeyScript(key: PublicKey) = Script.pay2sh(Script.pay2wpkh(key))
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key public key
|
||||
* @return the hash of the public key script for this key, as used by ElectrumX's hash-based methods
|
||||
*/
|
||||
def computeScriptHashFromPublicKey(key: PublicKey): BinaryData = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse
|
||||
|
||||
/**
|
||||
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
|
||||
*
|
||||
* @param master master key
|
||||
* @return the BIP49 account key for this master key: m/49'/1'/0'/0
|
||||
*/
|
||||
def accountKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
|
||||
|
||||
/**
|
||||
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
|
||||
*
|
||||
* @param master master key
|
||||
* @return the BIP49 change key for this master key: m/49'/1'/0'/1
|
||||
*/
|
||||
def changeKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
|
||||
|
||||
def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum)
|
||||
|
||||
def totalAmount(utxos: Set[Utxo]): Satoshi = totalAmount(utxos.toSeq)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param weight transaction weight
|
||||
* @param feeRatePerKw fee rate
|
||||
* @return the fee for this tx weight
|
||||
*/
|
||||
def computeFee(weight: Int, feeRatePerKw: Long): Satoshi = Satoshi((weight * feeRatePerKw) / 1000)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param txIn transaction input
|
||||
* @return Some(pubkey) if this tx input spends a p2sh-of-p2wpkh(pub), None otherwise
|
||||
*/
|
||||
def extractPubKeySpentFrom(txIn: TxIn): Option[PublicKey] = {
|
||||
Try {
|
||||
// we're looking for tx that spend a pay2sh-of-p2wkph output
|
||||
require(txIn.witness.stack.size == 2)
|
||||
val sig = txIn.witness.stack(0)
|
||||
val pub = txIn.witness.stack(1)
|
||||
val OP_PUSHDATA(script, _) :: Nil = Script.parse(txIn.signatureScript)
|
||||
val publicKey = PublicKey(pub)
|
||||
if (Script.write(Script.pay2wpkh(publicKey)) == script) {
|
||||
Some(publicKey)
|
||||
} else None
|
||||
} getOrElse None
|
||||
}
|
||||
|
||||
def computeDepth(currentHeight: Long, txHeight: Long): Long = currentHeight - txHeight + 1
|
||||
|
||||
case class Utxo(key: ExtendedPrivateKey, item: ElectrumClient.UnspentItem) {
|
||||
def outPoint: OutPoint = item.outPoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet state, which stores data returned by EletrumX servers.
|
||||
* Most items are indexed by script hash (i.e. by pubkey script sha256 hash).
|
||||
* Height follow ElectrumX's conventions:
|
||||
* - h > 0 means that the tx was confirmed at block #h
|
||||
* - 0 means unconfirmed, but all input are confirmed
|
||||
* < 0 means unconfirmed, and sonme inputs are unconfirmed as well
|
||||
*
|
||||
* @param tip current blockchain tip
|
||||
* @param accountKeys account keys
|
||||
* @param changeKeys change keys
|
||||
* @param status script hash -> status; "" means that the script hash has not been used
|
||||
* yet
|
||||
* @param transactions wallet transactions
|
||||
* @param heights transactions heights
|
||||
* @param history script hash -> history
|
||||
* @param locks transactions which lock some of our utxos.
|
||||
*/
|
||||
case class Data(tip: ElectrumClient.Header,
|
||||
accountKeys: Vector[ExtendedPrivateKey],
|
||||
changeKeys: Vector[ExtendedPrivateKey],
|
||||
status: Map[BinaryData, String],
|
||||
transactions: Map[BinaryData, Transaction],
|
||||
heights: Map[BinaryData, Long],
|
||||
history: Map[BinaryData, Seq[ElectrumClient.TransactionHistoryItem]],
|
||||
locks: Set[Transaction],
|
||||
pendingHistoryRequests: Set[BinaryData],
|
||||
pendingTransactionRequests: Set[BinaryData],
|
||||
pendingTransactions: Seq[Transaction]) extends Logging {
|
||||
lazy val accountKeyMap = accountKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap
|
||||
|
||||
lazy val changeKeyMap = changeKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap
|
||||
|
||||
lazy val firstUnusedAccountKeys = accountKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some(""))
|
||||
|
||||
lazy val firstUnusedChangeKeys = changeKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some(""))
|
||||
|
||||
lazy val publicScriptMap = (accountKeys ++ changeKeys).map(key => Script.write(computePublicKeyScript(key.publicKey)) -> key).toMap
|
||||
|
||||
lazy val utxos = history.keys.toSeq.map(scriptHash => getUtxos(scriptHash)).flatten
|
||||
|
||||
/**
|
||||
* The wallet is ready if all current keys have an empty status, and we don't have
|
||||
* any history/tx request pending
|
||||
* NB: swipeRange * 2 because we have account keys and change keys
|
||||
*/
|
||||
def isReady(swipeRange: Int) = status.filter(_._2 == "").size >= swipeRange * 2 && pendingHistoryRequests.isEmpty && pendingTransactionRequests.isEmpty
|
||||
|
||||
def readyMessage: WalletReady = {
|
||||
val (confirmed, unconfirmed) = balance
|
||||
WalletReady(confirmed, unconfirmed, tip.block_height)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the current receive key. In most cases it will be a key that has not
|
||||
* been used yet but it may be possible that we are still looking for
|
||||
* unused keys and none is available yet. In this case we will return
|
||||
* the latest account key.
|
||||
*/
|
||||
def currentReceiveKey = firstUnusedAccountKeys.headOption.getOrElse {
|
||||
// bad luck we are still looking for unused keys
|
||||
// use the first account key
|
||||
accountKeys.head
|
||||
}
|
||||
|
||||
def currentReceiveAddress = segwitAddress(currentReceiveKey)
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the current change key. In most cases it will be a key that has not
|
||||
* been used yet but it may be possible that we are still looking for
|
||||
* unused keys and none is available yet. In this case we will return
|
||||
* the latest change key.
|
||||
*/
|
||||
def currentChangeKey = firstUnusedChangeKeys.headOption.getOrElse {
|
||||
// bad luck we are still looking for unused keys
|
||||
// use the first account key
|
||||
changeKeys.head
|
||||
}
|
||||
|
||||
def currentChangeAddress = segwitAddress(currentChangeKey)
|
||||
|
||||
def isMine(txIn: TxIn): Boolean = extractPubKeySpentFrom(txIn).exists(pub => publicScriptMap.contains(Script.write(computePublicKeyScript(pub))))
|
||||
|
||||
def isSpend(txIn: TxIn, publicKey: PublicKey): Boolean = extractPubKeySpentFrom(txIn).contains(publicKey)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param txIn
|
||||
* @param scriptHash
|
||||
* @return true if txIn spends from an address that matches scriptHash
|
||||
*/
|
||||
def isSpend(txIn: TxIn, scriptHash: BinaryData): Boolean = extractPubKeySpentFrom(txIn).exists(pub => computeScriptHashFromPublicKey(pub) == scriptHash)
|
||||
|
||||
def isReceive(txOut: TxOut, scriptHash: BinaryData): Boolean = publicScriptMap.get(txOut.publicKeyScript).exists(key => computeScriptHashFromPublicKey(key.publicKey) == scriptHash)
|
||||
|
||||
def isMine(txOut: TxOut): Boolean = publicScriptMap.contains(txOut.publicKeyScript)
|
||||
|
||||
def computeTransactionDepth(txid: BinaryData): Long = heights.get(txid).map(height => if (height > 0) computeDepth(tip.block_height, height) else 0).getOrElse(0)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scriptHash script hash
|
||||
* @return the list of UTXOs for this script hash (including unconfirmed UTXOs)
|
||||
*/
|
||||
def getUtxos(scriptHash: BinaryData) = {
|
||||
history.get(scriptHash) match {
|
||||
case None => Seq()
|
||||
case Some(items) if items.isEmpty => Seq()
|
||||
case Some(items) =>
|
||||
// this is the private key for this script hash
|
||||
val key = accountKeyMap.getOrElse(scriptHash, changeKeyMap(scriptHash))
|
||||
|
||||
// find all transactions that send to or receive from this script hash
|
||||
// we use collect because we may not yet have received all transactions in the history
|
||||
val txs = items collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
|
||||
|
||||
// find all tx outputs that send to our script hash
|
||||
val unspents = items collect { case item if transactions.contains(item.tx_hash) =>
|
||||
val tx = transactions(item.tx_hash)
|
||||
val outputs = tx.txOut.zipWithIndex.filter { case (txOut, index) => isReceive(txOut, scriptHash) }
|
||||
outputs.map { case (txOut, index) => Utxo(key, ElectrumClient.UnspentItem(item.tx_hash, index, txOut.amount.toLong, item.height)) }
|
||||
} flatten
|
||||
|
||||
// and remove the outputs that are being spent. this is needed because we may have unconfirmed UTXOs
|
||||
// that are spend by unconfirmed transactions
|
||||
unspents.filterNot(utxo => txs.exists(tx => tx.txIn.exists(_.outPoint == utxo.outPoint)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scriptHash script hash
|
||||
* @return the (confirmed, unconfirmed) balance for this script hash. This balance may not
|
||||
* be up-to-date if we have not received all data we've asked for yet.
|
||||
*/
|
||||
def balance(scriptHash: BinaryData): (Satoshi, Satoshi) = {
|
||||
history.get(scriptHash) match {
|
||||
case None => (Satoshi(0), Satoshi(0))
|
||||
|
||||
case Some(items) if items.isEmpty => (Satoshi(0), Satoshi(0))
|
||||
|
||||
case Some(items) =>
|
||||
val (confirmedItems, unconfirmedItems) = items.partition(_.height > 0)
|
||||
val confirmedTxs = confirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
|
||||
val unconfirmedTxs = unconfirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) }
|
||||
if (confirmedTxs.size + unconfirmedTxs.size < confirmedItems.size + unconfirmedItems.size) logger.warn(s"we have not received all transactions yet, balance will not be up to date")
|
||||
|
||||
def findOurSpentOutputs(txs: Seq[Transaction]): Seq[TxOut] = {
|
||||
val inputs = txs.map(_.txIn).flatten.filter(txIn => isSpend(txIn, scriptHash))
|
||||
val spentOutputs = inputs.map(_.outPoint).map(outPoint => transactions.get(outPoint.txid).map(_.txOut(outPoint.index.toInt))).flatten
|
||||
spentOutputs
|
||||
}
|
||||
|
||||
val confirmedSpents = findOurSpentOutputs(confirmedTxs)
|
||||
val confirmedReceived = confirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash))
|
||||
|
||||
val unconfirmedSpents = findOurSpentOutputs(unconfirmedTxs)
|
||||
val unconfirmedReceived = unconfirmedTxs.map(_.txOut).flatten.filter(txOut => isReceive(txOut, scriptHash))
|
||||
|
||||
val confirmedBalance = confirmedReceived.map(_.amount).sum - confirmedSpents.map(_.amount).sum
|
||||
val unconfirmedBalance = unconfirmedReceived.map(_.amount).sum - unconfirmedSpents.map(_.amount).sum
|
||||
|
||||
(confirmedBalance, unconfirmedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the (confirmed, unconfirmed) balance for this wallet. This balance may not
|
||||
* be up-to-date if we have not received all data we've asked for yet.
|
||||
*/
|
||||
lazy val balance: (Satoshi, Satoshi) = {
|
||||
(accountKeyMap.keys ++ changeKeyMap.keys).map(scriptHash => balance(scriptHash)).foldLeft((Satoshi(0), Satoshi(0))) {
|
||||
case ((confirmed, unconfirmed), (confirmed1, unconfirmed1)) => (confirmed + confirmed1, unconfirmed + unconfirmed1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the effect of this transaction on the wallet
|
||||
*
|
||||
* @param tx input transaction
|
||||
* @return an option:
|
||||
* - Some(received, sent, fee) where sent if what the tx spends from us, received is what the tx sends to us,
|
||||
* and fee is the fee for the tx) tuple where sent if what the tx spends from us, and received is what the tx sends to us
|
||||
* - None if we are missing one or more parent txs
|
||||
*/
|
||||
def computeTransactionDelta(tx: Transaction): Option[(Satoshi, Satoshi, Option[Satoshi])] = {
|
||||
val ourInputs = tx.txIn.filter(isMine)
|
||||
// we need to make sure that for all inputs spending an output we control, we already have the parent tx
|
||||
// (otherwise we can't estimate our balance)
|
||||
val missingParent = ourInputs.exists(txIn => !transactions.contains(txIn.outPoint.txid))
|
||||
if (missingParent) {
|
||||
None
|
||||
} else {
|
||||
val sent = ourInputs.map(txIn => transactions(txIn.outPoint.txid).txOut(txIn.outPoint.index.toInt)).map(_.amount).sum
|
||||
val received = tx.txOut.filter(isMine).map(_.amount).sum
|
||||
// if all the inputs were ours, we can compute the fee, otherwise we can't
|
||||
val fee_opt = if (ourInputs.size == tx.txIn.size) Some(sent - tx.txOut.map(_.amount).sum) else None
|
||||
Some((received, sent, fee_opt))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tx input tx that has no inputs
|
||||
* @param feeRatePerKw fee rate per kiloweight
|
||||
* @param minimumFee minimum fee
|
||||
* @param dustLimit dust limit
|
||||
* @return a (state, tx) tuple where state has been updated and tx is a complete,
|
||||
* fully signed transaction that can be broadcast.
|
||||
* our utxos spent by this tx are locked and won't be available for spending
|
||||
* until the tx has been cancelled. If the tx is committed, they will be removed
|
||||
*/
|
||||
def completeTransaction(tx: Transaction, feeRatePerKw: Long, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction) = {
|
||||
require(tx.txIn.isEmpty, "cannot complete a tx that already has inputs")
|
||||
require(feeRatePerKw >= 0, "fee rate cannot be negative")
|
||||
val amount = tx.txOut.map(_.amount).sum
|
||||
require(amount > dustLimit, "amount to send is below dust limit")
|
||||
val fee = {
|
||||
val estimatedFee = computeFee(700, feeRatePerKw)
|
||||
if (estimatedFee < minimumFee) minimumFee else estimatedFee
|
||||
}
|
||||
|
||||
@tailrec
|
||||
def select(chooseFrom: Seq[Utxo], selected: Set[Utxo]): Set[Utxo] = {
|
||||
if (totalAmount(selected) >= amount + fee) selected
|
||||
else if (chooseFrom.isEmpty) Set()
|
||||
else select(chooseFrom.tail, selected + chooseFrom.head)
|
||||
}
|
||||
|
||||
// select utxos that are not locked by pending txs
|
||||
val lockedOutputs = locks.map(_.txIn.map(_.outPoint)).flatten
|
||||
val unlocked = utxos.filterNot(utxo => lockedOutputs.contains(utxo.outPoint))
|
||||
val unlocked1 = if (allowSpendUnconfirmed) unlocked else unlocked.filter(_.item.height > 0)
|
||||
val selected = select(unlocked1, Set()).toSeq
|
||||
require(totalAmount(selected) >= amount + fee, "insufficient funds")
|
||||
|
||||
// add inputs
|
||||
var tx1 = tx.copy(txIn = selected.map(utxo => TxIn(utxo.outPoint, Nil, TxIn.SEQUENCE_FINAL)))
|
||||
|
||||
// add change output
|
||||
val change = totalAmount(selected) - amount - fee
|
||||
if (change >= dustLimit) tx1 = tx1.addOutput(TxOut(change, computePublicKeyScript(currentChangeKey.publicKey)))
|
||||
|
||||
// sign
|
||||
for (i <- 0 until tx1.txIn.size) {
|
||||
val key = selected(i).key
|
||||
val sig = Transaction.signInput(tx1, i, Script.pay2pkh(key.publicKey), SIGHASH_ALL, Satoshi(selected(i).item.value), SigVersion.SIGVERSION_WITNESS_V0, key.privateKey)
|
||||
tx1 = tx1.updateWitness(i, ScriptWitness(sig :: key.publicKey.toBin :: Nil)).updateSigScript(i, OP_PUSHDATA(Script.write(Script.pay2wpkh(key.publicKey))) :: Nil)
|
||||
}
|
||||
Transaction.correctlySpends(tx1, selected.map(utxo => utxo.outPoint -> TxOut(Satoshi(utxo.item.value), computePublicKeyScript(utxo.key.publicKey))).toMap, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
|
||||
val data1 = this.copy(locks = this.locks + tx1)
|
||||
(data1, tx1)
|
||||
}
|
||||
|
||||
/**
|
||||
* unlocks input locked by a pending tx. call this method if the tx will not be used after all
|
||||
*
|
||||
* @param tx pending transaction
|
||||
* @return an updated state
|
||||
*/
|
||||
def cancelTransaction(tx: Transaction): Data = this.copy(locks = this.locks - tx)
|
||||
|
||||
/**
|
||||
* remove all our utxos spent by this tx. call this method if the tx was broadcast successfully
|
||||
*
|
||||
* @param tx pending transaction
|
||||
* @return an updated state
|
||||
*/
|
||||
def commitTransaction(tx: Transaction): Data = {
|
||||
// HACK! since we base our utxos computation on the history as seen by the electrum server (so that it is
|
||||
// reorg-proof out of the box), we need to update the history right away if we want to be able to build chained
|
||||
// unconfirmed transactions. A few seconds later electrum will notify us and the entry will be overwritten.
|
||||
// Note that we need to take into account both inputs and outputs, because there may be change.
|
||||
val history1 = (tx.txIn.filter(isMine).map(extractPubKeySpentFrom).flatten.map(computeScriptHashFromPublicKey) ++ tx.txOut.filter(isMine).map(_.publicKeyScript).map(computeScriptHash))
|
||||
.foldLeft(this.history) {
|
||||
case (history, scriptHash) =>
|
||||
val entry = history.get(scriptHash) match {
|
||||
case None => Seq(TransactionHistoryItem(0, tx.txid))
|
||||
case Some(items) if items.map(_.tx_hash).contains(tx.txid) => items
|
||||
case Some(items) => items :+ TransactionHistoryItem(0, tx.txid)
|
||||
}
|
||||
history + (scriptHash -> entry)
|
||||
}
|
||||
this.copy(locks = this.locks - tx, transactions = this.transactions + (tx.txid -> tx), heights = this.heights + (tx.txid -> 0L), history = history1)
|
||||
}
|
||||
}
|
||||
|
||||
object Data {
|
||||
def apply(params: ElectrumWallet.WalletParameters, tip: ElectrumClient.Header, accountKeys: Vector[ExtendedPrivateKey], changeKeys: Vector[ExtendedPrivateKey]): Data
|
||||
= Data(tip, accountKeys, changeKeys, Map(), Map(), Map(), Map(), Set(), Set(), Set(), Seq())
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
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.{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, fromShortId}
|
||||
|
||||
import scala.collection.SortedMap
|
||||
|
||||
|
||||
class ElectrumWatcher(client: ActorRef) extends Actor with Stash with ActorLogging {
|
||||
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
|
||||
override def unhandled(message: Any): Unit = message match {
|
||||
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 (_, _, 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 _ => 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 =>
|
||||
client ! ElectrumClient.HeaderSubscription(self)
|
||||
case ElectrumClient.HeaderSubscriptionResponse(header) =>
|
||||
watches.map(self ! _)
|
||||
publishQueue.map(self ! _)
|
||||
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(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(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(_ <= newtip.block_height)
|
||||
toPublish.values.flatten.foreach(tx => self ! PublishAsap(tx))
|
||||
context become running(newtip, watches, scriptHashStatus, block2tx -- toPublish.keys, sent)
|
||||
|
||||
case watch: Watch if watches.contains(watch) => ()
|
||||
|
||||
case watch@WatchSpent(_, txid, outputIndex, publicKeyScript, _) =>
|
||||
val scriptHash = computeScriptHash(publicKeyScript)
|
||||
log.info(s"added watch-spent on output=$txid:$outputIndex scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
|
||||
context.watch(watch.channel)
|
||||
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(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(tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case Terminated(actor) =>
|
||||
val watches1 = watches.filterNot(_.channel == actor)
|
||||
context become running(tip, watches1, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status) =>
|
||||
scriptHashStatus.get(scriptHash) match {
|
||||
case Some(s) if s == status => log.debug(s"already have status=$status for scriptHash=$scriptHash")
|
||||
case _ if status.isEmpty => log.info(s"empty status for scriptHash=$scriptHash")
|
||||
case _ =>
|
||||
log.info(s"new status=$status for scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
}
|
||||
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(height, tx_hash) if height > 0 => watches.collect {
|
||||
case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx_hash =>
|
||||
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, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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=$height pos=$pos")
|
||||
channel ! WatchEventConfirmed(event, height.toInt, pos)
|
||||
w
|
||||
}
|
||||
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case ElectrumClient.GetTransactionResponse(spendingTx) =>
|
||||
val triggered = spendingTx.txIn.map(_.outPoint).flatMap(outPoint => watches.collect {
|
||||
case WatchSpent(channel, txid, pos, _, event) if txid == outPoint.txid && pos == outPoint.index.toInt =>
|
||||
log.info(s"output $txid:$pos spent by transaction ${spendingTx.txid}")
|
||||
channel ! WatchEventSpent(event, spendingTx)
|
||||
// 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)
|
||||
None
|
||||
case w@WatchSpentBasic(channel, txid, pos, _, event) if txid == outPoint.txid && pos == outPoint.index.toInt =>
|
||||
log.info(s"output $txid:$pos spent by transaction ${spendingTx.txid}")
|
||||
channel ! WatchEventSpentBasic(event)
|
||||
Some(w)
|
||||
}).flatten
|
||||
context become running(tip, watches -- triggered, scriptHashStatus, block2tx, 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 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(tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
} else {
|
||||
log.info(s"publishing tx=${Transaction.write(tx)}")
|
||||
client ! BroadcastTransaction(tx)
|
||||
context become running(tip, watches, scriptHashStatus, block2tx, sent :+ 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 running(tip, watches, scriptHashStatus, block2tx1, sent)
|
||||
} else {
|
||||
log.info(s"publishing tx=${Transaction.write(tx)}")
|
||||
client ! BroadcastTransaction(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=${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(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 previsouly sent but hadn't yet received the confirmation
|
||||
context become disconnected(watches, sent.map(PublishAsap(_)), block2tx)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ElectrumWatcher extends App {
|
||||
|
||||
val system = ActorSystem()
|
||||
|
||||
class Root extends Actor with ActorLogging {
|
||||
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 = {
|
||||
super.unhandled(message)
|
||||
log.warning(s"unhandled message $message")
|
||||
}
|
||||
|
||||
def receive = {
|
||||
case ElectrumClient.ElectrumReady =>
|
||||
log.info(s"starting watcher")
|
||||
context become running(context.actorOf(Props(new ElectrumWatcher(client)), "watcher"))
|
||||
}
|
||||
|
||||
def running(watcher: ActorRef): Receive = {
|
||||
case watch: Watch => watcher forward watch
|
||||
}
|
||||
}
|
||||
|
||||
val root = system.actorOf(Props[Root], "root")
|
||||
val scanner = new java.util.Scanner(System.in)
|
||||
while (true) {
|
||||
val tx = Transaction.read(scanner.nextLine())
|
||||
root ! WatchSpent(root, tx.txid, 0, tx.txOut(0).publicKeyScript, BITCOIN_FUNDING_SPENT)
|
||||
root ! WatchConfirmed(root, tx.txid, tx.txOut(0).publicKeyScript, 4L, BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import fr.acinq.bitcoin.Btc
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
|
||||
import org.json4s.JsonAST.{JDouble, JInt}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
def estimateSmartFee(nBlocks: Int): Future[Long] =
|
||||
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[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 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)
|
||||
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class ConstantFeeProvider(feerates: FeeratesPerByte) extends FeeProvider {
|
||||
|
||||
override def getFeerates: Future[FeeratesPerByte] = Future.successful(feerates)
|
||||
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
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.{DefaultFormats, jackson}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 16/11/2017.
|
||||
*/
|
||||
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
|
||||
|
||||
override def getFeerates: Future[FeeratesPerByte] =
|
||||
for {
|
||||
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)
|
||||
}
|
||||
|
||||
object EarnDotComFeeProvider {
|
||||
|
||||
case class FeeRange(minFee: Long, maxFee: Long, memCount: Long, minDelay: Long, maxDelay: Long)
|
||||
|
||||
def parseFeeRanges(json: JValue): Seq[FeeRange] = {
|
||||
val JArray(items) = json \ "fees"
|
||||
items.map(item => {
|
||||
val JInt(minFee) = item \ "minFee"
|
||||
val JInt(maxFee) = item \ "maxFee"
|
||||
val JInt(memCount) = item \ "memCount"
|
||||
val JInt(minDelay) = item \ "minDelay"
|
||||
val JInt(maxDelay) = item \ "maxDelay"
|
||||
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
|
||||
belowLimit.minBy(_.maxFee).maxFee
|
||||
}
|
||||
|
||||
def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerByte =
|
||||
FeeratesPerByte(
|
||||
block_1 = extractFeerate(feeRanges, 1),
|
||||
blocks_2 = extractFeerate(feeRanges, 2),
|
||||
blocks_6 = extractFeerate(feeRanges, 6),
|
||||
blocks_12 = extractFeerate(feeRanges, 12),
|
||||
blocks_36 = extractFeerate(feeRanges, 36),
|
||||
blocks_72 = extractFeerate(feeRanges, 72))
|
||||
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
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
|
||||
*/
|
||||
class FallbackFeeProvider(providers: Seq[FeeProvider])(implicit ec: ExecutionContext) extends FeeProvider {
|
||||
|
||||
require(providers.size >= 1, "need at least one fee provider")
|
||||
|
||||
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[FeeratesPerByte] = getFeerates(providers)
|
||||
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import fr.acinq.eclair.feerateByte2Kw
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
trait FeeProvider {
|
||||
|
||||
def getFeerates: Future[FeeratesPerByte]
|
||||
|
||||
}
|
||||
|
||||
case class FeeratesPerByte(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long)
|
||||
|
||||
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: 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
|
||||
*
|
||||
* @param feeratePerKw
|
||||
* @return
|
||||
*/
|
||||
def single(feeratePerKw: Long): FeeratesPerKw = FeeratesPerKw(
|
||||
block_1 = feeratePerKw,
|
||||
blocks_2 = feeratePerKw,
|
||||
blocks_6 = feeratePerKw,
|
||||
blocks_12 = feeratePerKw,
|
||||
blocks_36 = feeratePerKw,
|
||||
blocks_72 = feeratePerKw)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,43 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.UInt64
|
||||
|
||||
/**
|
||||
* Created by PM on 11/04/2017.
|
||||
*/
|
||||
|
||||
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message)
|
||||
// @formatter:off
|
||||
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
|
||||
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
|
||||
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
|
||||
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
|
||||
case class CannotCloseWithUnsignedOutgoingHtlcs(channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
|
||||
case class ChannelUnavailable (channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
|
||||
case class InvalidFinalScript (channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
|
||||
case class HtlcTimedout (channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
|
||||
case class FeerateTooDifferent (channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
case class InvalidCloseSignature (channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
|
||||
case class InvalidCommitmentSignature (channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
|
||||
case class ForcedLocalCommit (channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
|
||||
case class UnexpectedHtlcId (channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
|
||||
case class InvalidPaymentHash (channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
|
||||
case class ExpiryTooSmall (channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
|
||||
case class ExpiryCannotBeInThePast (channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
|
||||
case class HtlcValueTooSmall (channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
|
||||
case class HtlcValueTooHighInFlight (channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
|
||||
case class TooManyAcceptedHtlcs (channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
|
||||
case class InsufficientFunds (channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
|
||||
case class InvalidHtlcPreimage (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
|
||||
case class UnknownHtlcId (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
|
||||
case class FundeeCannotSendUpdateFee (channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
|
||||
case class CannotAffordFees (channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
|
||||
case class CannotSignWithoutChanges (channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
|
||||
case class CannotSignBeforeRevocation (channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
|
||||
case class UnexpectedRevocation (channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
|
||||
case class InvalidRevocation (channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
|
||||
case class CommitmentSyncError (channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
|
||||
case class RevocationSyncError (channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
|
||||
case class InvalidFailureCode (channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
|
||||
// @formatter:on
|
||||
@ -1,524 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
|
||||
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 grizzled.slf4j.Logging
|
||||
|
||||
// @formatter:off
|
||||
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
|
||||
def all: List[UpdateMessage] = proposed ++ signed ++ acked
|
||||
}
|
||||
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: 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: BinaryData, remotePerCommitmentPoint: Point)
|
||||
case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false)
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* about remoteNextCommitInfo:
|
||||
* we either:
|
||||
* - have built and signed their next commit tx with their next revocation hash which can now be discarded
|
||||
* - have their next per-commitment point
|
||||
* So, when we've signed and sent a commit message and are waiting for their revocation message,
|
||||
* theirNextCommitInfo is their next commit tx. The rest of the time, it is their next per-commitment point
|
||||
*/
|
||||
case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
|
||||
channelFlags: Byte,
|
||||
localCommit: LocalCommit, remoteCommit: RemoteCommit,
|
||||
localChanges: LocalChanges, remoteChanges: RemoteChanges,
|
||||
localNextHtlcId: Long, remoteNextHtlcId: Long,
|
||||
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: BinaryData) {
|
||||
|
||||
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
object Commitments extends Logging {
|
||||
/**
|
||||
* add a change to our proposed change list
|
||||
*
|
||||
* @param commitments
|
||||
* @param proposal
|
||||
* @return an updated commitment instance
|
||||
*/
|
||||
private def addLocalProposal(commitments: Commitments, proposal: UpdateMessage): Commitments =
|
||||
commitments.copy(localChanges = commitments.localChanges.copy(proposed = commitments.localChanges.proposed :+ proposal))
|
||||
|
||||
private def addRemoteProposal(commitments: Commitments, proposal: UpdateMessage): Commitments =
|
||||
commitments.copy(remoteChanges = commitments.remoteChanges.copy(proposed = commitments.remoteChanges.proposed :+ proposal))
|
||||
|
||||
/**
|
||||
*
|
||||
* @param commitments current commitments
|
||||
* @param cmd add HTLC command
|
||||
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc)
|
||||
*/
|
||||
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
|
||||
|
||||
if (cmd.paymentHash.size != 32) {
|
||||
return Left(InvalidPaymentHash(commitments.channelId))
|
||||
}
|
||||
|
||||
val blockCount = Globals.blockCount.get()
|
||||
if (cmd.expiry <= blockCount) {
|
||||
return Left(ExpiryCannotBeInThePast(commitments.channelId, cmd.expiry, blockCount))
|
||||
}
|
||||
|
||||
if (cmd.amountMsat < commitments.remoteParams.htlcMinimumMsat) {
|
||||
return Left(HtlcValueTooSmall(commitments.channelId, minimum = commitments.remoteParams.htlcMinimumMsat, actual = cmd.amountMsat))
|
||||
}
|
||||
|
||||
// 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.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)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
|
||||
// we look from remote's point of view, so if local is funder remote doesn't pay the fees
|
||||
val fees = if (commitments1.localParams.isFunder) Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount else 0
|
||||
val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees
|
||||
if (missing < 0) {
|
||||
return Left(InsufficientFunds(commitments.channelId, amountMsat = cmd.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.remoteParams.channelReserveSatoshis, feesSatoshis = fees))
|
||||
}
|
||||
|
||||
Right(commitments1, add)
|
||||
}
|
||||
|
||||
def receiveAdd(commitments: Commitments, add: UpdateAddHtlc): Commitments = {
|
||||
if (add.id != commitments.remoteNextHtlcId) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 htlcValueInFlight = UInt64(reduced.htlcs.map(_.add.amountMsat).sum)
|
||||
if (htlcValueInFlight > commitments1.localParams.maxHtlcValueInFlightMsat) {
|
||||
throw HtlcValueTooHighInFlight(commitments.channelId, maximum = commitments1.localParams.maxHtlcValueInFlightMsat, actual = htlcValueInFlight)
|
||||
}
|
||||
|
||||
val acceptedHtlcs = reduced.htlcs.count(_.direction == IN)
|
||||
if (acceptedHtlcs > commitments1.localParams.maxAcceptedHtlcs) {
|
||||
throw TooManyAcceptedHtlcs(commitments.channelId, maximum = commitments1.localParams.maxAcceptedHtlcs)
|
||||
}
|
||||
|
||||
// a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee
|
||||
val fees = if (commitments1.localParams.isFunder) 0 else Transactions.commitTxFee(Satoshi(commitments1.localParams.dustLimitSatoshis), reduced).amount
|
||||
val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees
|
||||
if (missing < 0) {
|
||||
throw InsufficientFunds(commitments.channelId, amountMsat = add.amountMsat, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
|
||||
}
|
||||
|
||||
commitments1
|
||||
}
|
||||
|
||||
def getHtlcCrossSigned(commitments: Commitments, directionRelativeToLocal: Direction, htlcId: Long): Option[UpdateAddHtlc] = {
|
||||
val remoteSigned = commitments.localCommit.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal && htlc.add.id == htlcId)
|
||||
val localSigned = commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit)
|
||||
.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal.opposite && htlc.add.id == htlcId)
|
||||
for {
|
||||
htlc_out <- remoteSigned
|
||||
htlc_in <- localSigned
|
||||
} yield htlc_in.add
|
||||
}
|
||||
|
||||
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) =
|
||||
getHtlcCrossSigned(commitments, IN, cmd.id) match {
|
||||
case Some(htlc) if commitments.localChanges.proposed.exists {
|
||||
case u: UpdateFulfillHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
|
||||
case _ => false
|
||||
} =>
|
||||
// we have already sent a fail/fulfill for this htlc
|
||||
throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
case Some(htlc) if htlc.paymentHash == sha256(cmd.r) =>
|
||||
val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r)
|
||||
val commitments1 = addLocalProposal(commitments, fulfill)
|
||||
(commitments1, fulfill)
|
||||
case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, cmd.id)
|
||||
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
}
|
||||
|
||||
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)))
|
||||
case Some(htlc) => throw InvalidHtlcPreimage(commitments.channelId, fulfill.id)
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fulfill.id)
|
||||
}
|
||||
|
||||
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): (Commitments, UpdateFailHtlc) =
|
||||
getHtlcCrossSigned(commitments, IN, cmd.id) match {
|
||||
case Some(htlc) if commitments.localChanges.proposed.exists {
|
||||
case u: UpdateFulfillHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
|
||||
case _ => false
|
||||
} =>
|
||||
// we have already sent a fail/fulfill for this htlc
|
||||
throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
case Some(htlc) =>
|
||||
// we need the shared secret to build the error packet
|
||||
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)
|
||||
}
|
||||
|
||||
def sendFailMalformed(commitments: Commitments, cmd: CMD_FAIL_MALFORMED_HTLC): (Commitments, UpdateFailMalformedHtlc) = {
|
||||
// BADONION bit must be set in failure_code
|
||||
if ((cmd.failureCode & FailureMessageCodecs.BADONION) == 0) {
|
||||
throw InvalidFailureCode(commitments.channelId)
|
||||
}
|
||||
getHtlcCrossSigned(commitments, IN, cmd.id) match {
|
||||
case Some(htlc) if commitments.localChanges.proposed.exists {
|
||||
case u: UpdateFulfillHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailHtlc if htlc.id == u.id => true
|
||||
case u: UpdateFailMalformedHtlc if htlc.id == u.id => true
|
||||
case _ => false
|
||||
} =>
|
||||
// we have already sent a fail/fulfill for this htlc
|
||||
throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
case Some(htlc) =>
|
||||
val fail = UpdateFailMalformedHtlc(commitments.channelId, cmd.id, cmd.onionHash, cmd.failureCode)
|
||||
val commitments1 = addLocalProposal(commitments, fail)
|
||||
(commitments1, fail)
|
||||
case None => throw UnknownHtlcId(commitments.channelId, cmd.id)
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
|
||||
}
|
||||
|
||||
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)))
|
||||
case None => throw UnknownHtlcId(commitments.channelId, fail.id)
|
||||
}
|
||||
}
|
||||
|
||||
def sendFee(commitments: Commitments, cmd: CMD_UPDATE_FEE): (Commitments, UpdateFee) = {
|
||||
if (!commitments.localParams.isFunder) {
|
||||
throw FundeeCannotSendUpdateFee(commitments.channelId)
|
||||
}
|
||||
// let's compute the current commitment *as seen by them* with this change taken into account
|
||||
val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw)
|
||||
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
|
||||
// we look from remote's point of view, so if local is funder remote doesn't pay the fees
|
||||
val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount
|
||||
val missing = reduced.toRemoteMsat / 1000 - commitments1.remoteParams.channelReserveSatoshis - fees
|
||||
if (missing < 0) {
|
||||
throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
|
||||
}
|
||||
|
||||
(commitments1, fee)
|
||||
}
|
||||
|
||||
def receiveFee(commitments: Commitments, fee: UpdateFee, maxFeerateMismatch: Double): Commitments = {
|
||||
if (commitments.localParams.isFunder) {
|
||||
throw FundeeCannotSendUpdateFee(commitments.channelId)
|
||||
}
|
||||
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) {
|
||||
throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)
|
||||
}
|
||||
|
||||
// NB: we check that the funder can afford this new fee even if spec allows to do it at next signature
|
||||
// It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid,
|
||||
// and it would be tricky to check if the conditions are met at signing
|
||||
// (it also means that we need to check the fee of the initial commitment tx somewhere)
|
||||
|
||||
// let's compute the current commitment *as seen by us* including this change
|
||||
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
|
||||
val fees = Transactions.commitTxFee(Satoshi(commitments1.remoteParams.dustLimitSatoshis), reduced).amount
|
||||
val missing = reduced.toRemoteMsat / 1000 - commitments1.localParams.channelReserveSatoshis - fees
|
||||
if (missing < 0) {
|
||||
throw CannotAffordFees(commitments.channelId, missingSatoshis = -1 * missing, reserveSatoshis = commitments1.localParams.channelReserveSatoshis, feesSatoshis = fees)
|
||||
}
|
||||
|
||||
commitments1
|
||||
}
|
||||
|
||||
def localHasUnsignedOutgoingHtlcs(commitments: Commitments): Boolean = commitments.localChanges.proposed.collectFirst { case u: UpdateAddHtlc => u }.isDefined
|
||||
|
||||
def remoteHasUnsignedOutgoingHtlcs(commitments: Commitments): Boolean = commitments.remoteChanges.proposed.collectFirst { case u: UpdateAddHtlc => u }.isDefined
|
||||
|
||||
def localHasChanges(commitments: Commitments): Boolean = commitments.remoteChanges.acked.size > 0 || commitments.localChanges.proposed.size > 0
|
||||
|
||||
def remoteHasChanges(commitments: Commitments): Boolean = commitments.localChanges.acked.size > 0 || commitments.remoteChanges.proposed.size > 0
|
||||
|
||||
def revocationPreimage(seed: BinaryData, index: Long): BinaryData = ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - index)
|
||||
|
||||
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index))
|
||||
|
||||
def sendCommit(commitments: Commitments): (Commitments, CommitSig) = {
|
||||
import commitments._
|
||||
commitments.remoteNextCommitInfo match {
|
||||
case Right(_) if !localHasChanges(commitments) =>
|
||||
throw CannotSignWithoutChanges(commitments.channelId)
|
||||
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(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 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(
|
||||
channelId = commitments.channelId,
|
||||
signature = sig,
|
||||
htlcSignatures = htlcSigs.toList
|
||||
)
|
||||
|
||||
val commitments1 = commitments.copy(
|
||||
remoteNextCommitInfo = Left(WaitingForRevocation(RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint), commitSig, commitments.localCommit.index)),
|
||||
localChanges = localChanges.copy(proposed = Nil, signed = localChanges.proposed),
|
||||
remoteChanges = remoteChanges.copy(acked = Nil, signed = remoteChanges.acked))
|
||||
(commitments1, commitSig)
|
||||
case Left(_) =>
|
||||
throw CannotSignBeforeRevocation(commitments.channelId)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// ourCommit.index -> our current revocation hash, which is about to become our old revocation hash
|
||||
// ourCommit.index + 1 -> our next revocation hash, used by *them* to build the sig we've just received, and which
|
||||
// is about to become our current revocation hash
|
||||
// ourCommit.index + 2 -> which is about to become our next revocation hash
|
||||
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
|
||||
// and will increment our index
|
||||
|
||||
if (!remoteHasChanges(commitments))
|
||||
throw CannotSignWithoutChanges(commitments.channelId)
|
||||
|
||||
// check that their signature is valid
|
||||
// signatures are now optional in the commit message, and will be sent only if the other party is actually
|
||||
// receiving money i.e its commit tx has one output for them
|
||||
|
||||
val spec = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed)
|
||||
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, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, sig, commit.signature)
|
||||
if (Transactions.checkSpendable(signedCommitTx).isFailure) {
|
||||
throw InvalidCommitmentSignature(commitments.channelId)
|
||||
}
|
||||
|
||||
val sortedHtlcTxs: Seq[TransactionWithInputInfo] = (htlcTimeoutTxs ++ htlcSuccessTxs).sortBy(_.input.outPoint.index)
|
||||
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) =>
|
||||
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
|
||||
require(Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey), "bad sig")
|
||||
HtlcTxAndSigs(htlcTx, localSig, remoteSig)
|
||||
}
|
||||
|
||||
// we will send our revocation preimage + our next revocation hash
|
||||
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,
|
||||
nextPerCommitmentPoint = localNextPerCommitmentPoint
|
||||
)
|
||||
|
||||
// update our commitment data
|
||||
val localCommit1 = LocalCommit(
|
||||
index = localCommit.index + 1,
|
||||
spec,
|
||||
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
|
||||
val ourChanges1 = localChanges.copy(acked = Nil)
|
||||
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
|
||||
// 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 = {
|
||||
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 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, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index))
|
||||
|
||||
commitments1
|
||||
case Right(_) =>
|
||||
throw UnexpectedRevocation(commitments.channelId)
|
||||
}
|
||||
}
|
||||
|
||||
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, 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(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(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)
|
||||
}
|
||||
|
||||
def msg2String(msg: LightningMessage): String = msg match {
|
||||
case u: UpdateAddHtlc => s"add-${u.id}"
|
||||
case u: UpdateFulfillHtlc => s"ful-${u.id}"
|
||||
case u: UpdateFailHtlc => s"fail-${u.id}"
|
||||
case _: UpdateFee => s"fee"
|
||||
case _: CommitSig => s"sig"
|
||||
case _: RevokeAndAck => s"rev"
|
||||
case _: Error => s"err"
|
||||
case _: FundingLocked => s"funding_locked"
|
||||
case _ => "???"
|
||||
}
|
||||
|
||||
def changes2String(commitments: Commitments): String = {
|
||||
import commitments._
|
||||
s"""commitments:
|
||||
| localChanges:
|
||||
| proposed: ${localChanges.proposed.map(msg2String(_)).mkString(" ")}
|
||||
| signed: ${localChanges.signed.map(msg2String(_)).mkString(" ")}
|
||||
| acked: ${localChanges.acked.map(msg2String(_)).mkString(" ")}
|
||||
| remoteChanges:
|
||||
| proposed: ${remoteChanges.proposed.map(msg2String(_)).mkString(" ")}
|
||||
| acked: ${remoteChanges.acked.map(msg2String(_)).mkString(" ")}
|
||||
| signed: ${remoteChanges.signed.map(msg2String(_)).mkString(" ")}
|
||||
| nextHtlcId:
|
||||
| local: $localNextHtlcId
|
||||
| remote: $remoteNextHtlcId""".stripMargin
|
||||
}
|
||||
|
||||
def specs2String(commitments: Commitments): String = {
|
||||
s"""specs:
|
||||
|localcommit:
|
||||
| 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.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.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.expiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef}
|
||||
import fr.acinq.eclair.NodeParams
|
||||
import fr.acinq.eclair.wire.LightningMessage
|
||||
|
||||
/**
|
||||
* Created by fabrice on 27/02/17.
|
||||
*/
|
||||
|
||||
class Forwarder(nodeParams: NodeParams) extends Actor with ActorLogging {
|
||||
|
||||
// caller is responsible for sending the destination before anything else
|
||||
// the general case is that destination can die anytime and it is managed by the caller
|
||||
def receive = main(context.system.deadLetters)
|
||||
|
||||
def main(destination: ActorRef): Receive = {
|
||||
|
||||
case destination: ActorRef => context become main(destination)
|
||||
|
||||
case msg: LightningMessage => destination forward msg
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,545 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256}
|
||||
import fr.acinq.bitcoin.Script._
|
||||
import fr.acinq.bitcoin.{OutPoint, _}
|
||||
import fr.acinq.eclair.blockchain.EclairWallet
|
||||
import fr.acinq.eclair.crypto.Generators
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Scripts._
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.{Globals, NodeParams}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by PM on 20/05/2016.
|
||||
*/
|
||||
|
||||
object Helpers {
|
||||
|
||||
/**
|
||||
* Depending on the state, returns the current temporaryChannelId or channelId
|
||||
*
|
||||
* @param stateData
|
||||
* @return
|
||||
*/
|
||||
def getChannelId(stateData: Data): BinaryData = stateData match {
|
||||
case Nothing => BinaryData("00" * 32)
|
||||
case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId
|
||||
case d: DATA_WAIT_FOR_ACCEPT_CHANNEL => d.initFunder.temporaryChannelId
|
||||
case d: DATA_WAIT_FOR_FUNDING_INTERNAL => d.temporaryChannelId
|
||||
case d: DATA_WAIT_FOR_FUNDING_CREATED => d.temporaryChannelId
|
||||
case d: DATA_WAIT_FOR_FUNDING_SIGNED => d.channelId
|
||||
case d: HasCommitments => d.channelId
|
||||
}
|
||||
|
||||
def validateParamsFunder(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long): Unit = {
|
||||
val reserveToFundingRatio = channelReserveSatoshis.toDouble / fundingSatoshis
|
||||
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
|
||||
throw new ChannelReserveTooHigh(temporaryChannelId, channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
|
||||
}
|
||||
}
|
||||
|
||||
def validateParamsFundee(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long, chainHash: BinaryData, initialFeeratePerKw: Long): Unit = {
|
||||
require(nodeParams.chainHash == chainHash, s"invalid chain hash $chainHash (we are on ${nodeParams.chainHash})")
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
// we are fundee => initialFeeratePerKw has been set by remote
|
||||
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) {
|
||||
throw new FeerateTooDifferent(temporaryChannelId, localFeeratePerKw, initialFeeratePerKw)
|
||||
}
|
||||
validateParamsFunder(temporaryChannelId, nodeParams, channelReserveSatoshis, fundingSatoshis)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param remoteFeeratePerKw remote fee rate per kiloweight
|
||||
* @param localFeeratePerKw local fee rate per kiloweight
|
||||
* @return the "normalized" difference between local and remote fee rate, i.e. |remote - local| / avg(local, remote)
|
||||
*/
|
||||
def feeRateMismatch(remoteFeeratePerKw: Long, localFeeratePerKw: Long): Double =
|
||||
Math.abs((2.0 * (remoteFeeratePerKw - localFeeratePerKw)) / (localFeeratePerKw + remoteFeeratePerKw))
|
||||
|
||||
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
|
||||
// negative feerate can happen in regtest mode
|
||||
networkFeeratePerKw > 0 && feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
|
||||
|
||||
/**
|
||||
*
|
||||
* @param remoteFeeratePerKw remote fee rate per kiloweight
|
||||
* @param localFeeratePerKw local fee rate per kiloweight
|
||||
* @param maxFeerateMismatchRatio maximum fee rate mismatch ratio
|
||||
* @return true if the difference between local and remote fee rates is too high.
|
||||
* the actual check is |remote - local| / avg(local, remote) > mismatch ratio
|
||||
*/
|
||||
def isFeeDiffTooHigh(remoteFeeratePerKw: Long, localFeeratePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = {
|
||||
// negative feerate can happen in regtest mode
|
||||
remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio
|
||||
}
|
||||
|
||||
def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: Long) = {
|
||||
// TODO: empty features
|
||||
val features = BinaryData("")
|
||||
val (localNodeSig, localBitcoinSig) = Announcements.signChannelAnnouncement(nodeParams.chainHash, shortChannelId, nodeParams.privateKey, commitments.remoteParams.nodeId, commitments.localParams.fundingPrivKey, commitments.remoteParams.fundingPubKey, features)
|
||||
AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig)
|
||||
}
|
||||
|
||||
def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = {
|
||||
import scala.concurrent.duration._
|
||||
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)
|
||||
val finalScriptPubKey = Base58Check.decode(finalAddress) match {
|
||||
case (Base58.Prefix.PubkeyAddressTestnet, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
|
||||
case (Base58.Prefix.ScriptAddressTestnet, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil)
|
||||
}
|
||||
finalScriptPubKey
|
||||
}
|
||||
|
||||
object Funding {
|
||||
|
||||
def makeFundingInputInfo(fundingTxId: BinaryData, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {
|
||||
val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2)
|
||||
val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript))
|
||||
InputInfo(OutPoint(fundingTxId, fundingTxOutputIndex), fundingTxOut, write(fundingScript))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates both sides's first commitment transaction
|
||||
*
|
||||
* @param localParams
|
||||
* @param remoteParams
|
||||
* @param pushMsat
|
||||
* @param fundingTxHash
|
||||
* @param fundingTxOutputIndex
|
||||
* @param remoteFirstPerCommitmentPoint
|
||||
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
|
||||
*/
|
||||
def makeFirstCommitTxs(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxHash: BinaryData, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: Point, maxFeerateMismatch: Double): (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx) = {
|
||||
val toLocalMsat = if (localParams.isFunder) fundingSatoshis * 1000 - pushMsat else pushMsat
|
||||
val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingSatoshis * 1000 - pushMsat
|
||||
|
||||
val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toLocalMsat, toRemoteMsat = toRemoteMsat)
|
||||
val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], feeratePerKw = initialFeeratePerKw, toLocalMsat = toRemoteMsat, toRemoteMsat = toLocalMsat)
|
||||
|
||||
if (!localParams.isFunder) {
|
||||
// they are funder, therefore they pay the fee: we need to make sure they can afford it!
|
||||
val toRemoteMsat = remoteSpec.toLocalMsat
|
||||
val fees = Transactions.commitTxFee(Satoshi(remoteParams.dustLimitSatoshis), remoteSpec).amount
|
||||
val missing = toRemoteMsat / 1000 - localParams.channelReserveSatoshis - fees
|
||||
if (missing < 0) {
|
||||
throw CannotAffordFees(temporaryChannelId, missingSatoshis = -1 * missing, reserveSatoshis = localParams.channelReserveSatoshis, feesSatoshis = fees)
|
||||
}
|
||||
}
|
||||
|
||||
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, Satoshi(fundingSatoshis), localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey)
|
||||
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, 0)
|
||||
val (localCommitTx, _, _) = Commitments.makeLocalTxs(0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
|
||||
val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
|
||||
|
||||
(localSpec, localCommitTx, remoteSpec, remoteCommitTx)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Closing extends Logging {
|
||||
|
||||
def isValidFinalScriptPubkey(scriptPubKey: BinaryData): Boolean = {
|
||||
Try(Script.parse(scriptPubKey)) match {
|
||||
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true
|
||||
case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true
|
||||
case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true
|
||||
case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
def makeFirstClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData): ClosingSigned = {
|
||||
logger.debug(s"making first closing tx with commitments:\n${Commitments.specs2String(commitments)}")
|
||||
import commitments._
|
||||
val closingFee = {
|
||||
// this is just to estimate the weight, it depends on size of the pubkey scripts
|
||||
val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
|
||||
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, "aa" * 71, "bb" * 71).tx)
|
||||
// no need to use a very high fee here
|
||||
val feeratePerKw = Globals.feeratesPerKw.get.blocks_6
|
||||
logger.info(s"using feeratePerKw=$feeratePerKw for closing tx")
|
||||
Transactions.weight2fee(feeratePerKw, closingWeight)
|
||||
}
|
||||
val (_, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
|
||||
closingSigned
|
||||
}
|
||||
|
||||
def makeClosingTx(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, closingFee: Satoshi): (ClosingTx, ClosingSigned) = {
|
||||
import commitments._
|
||||
require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey")
|
||||
require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey")
|
||||
// TODO: check that
|
||||
val dustLimitSatoshis = Satoshi(Math.max(localParams.dustLimitSatoshis, remoteParams.dustLimitSatoshis))
|
||||
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
|
||||
val localClosingSig = Transactions.sign(closingTx, commitments.localParams.fundingPrivKey)
|
||||
val closingSigned = ClosingSigned(channelId, closingFee.amount, localClosingSig)
|
||||
logger.debug(s"closingTx=${Transaction.write(closingTx.tx)}")
|
||||
(closingTx, closingSigned)
|
||||
}
|
||||
|
||||
def checkClosingSignature(commitments: Commitments, localScriptPubkey: BinaryData, remoteScriptPubkey: BinaryData, remoteClosingFee: Satoshi, remoteClosingSig: BinaryData): Try[Transaction] = {
|
||||
import commitments._
|
||||
val (closingTx, closingSigned) = makeClosingTx(commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
|
||||
val signedClosingTx = Transactions.addSigs(closingTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
|
||||
Transactions.checkSpendable(signedClosingTx).map(x => signedClosingTx.tx)
|
||||
}
|
||||
|
||||
def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
|
||||
|
||||
def generateTx(desc: String)(attempt: Try[TransactionWithInputInfo]): Option[TransactionWithInputInfo] = {
|
||||
attempt match {
|
||||
case Success(txinfo) =>
|
||||
logger.warn(s"tx generation success: desc=$desc txid=${txinfo.tx.txid} amount=${txinfo.tx.txOut.map(_.amount.amount).sum} tx=${Transaction.write(txinfo.tx)}")
|
||||
Some(txinfo)
|
||||
case Failure(t) =>
|
||||
logger.warn(s"tx generation failure: desc=$desc reason: ${t.getMessage}")
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Claim all the HTLCs that we've received from our current commit tx. This will be
|
||||
* done using 2nd stage HTLC transactions
|
||||
*
|
||||
* @param commitments our commitment data, which include payment preimages
|
||||
* @return a list of transactions (one per HTLC that we can claim)
|
||||
*/
|
||||
def claimCurrentLocalCommitTxOutputs(commitments: Commitments, tx: Transaction): LocalCommitPublished = {
|
||||
import commitments._
|
||||
require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
|
||||
|
||||
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index.toInt)
|
||||
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
|
||||
val localDelayedPrivkey = Generators.derivePrivKey(localParams.delayedPaymentKey, localPerCommitmentPoint)
|
||||
|
||||
// no need to use a high fee rate for delayed transactions (we are the only one who can spend them)
|
||||
val feeratePerKwDelayed = Globals.feeratesPerKw.get.blocks_6
|
||||
|
||||
// first we will claim our main output as soon as the delay is over
|
||||
val mainDelayedTx = generateTx("main-delayed-output")(Try {
|
||||
val claimDelayed = Transactions.makeClaimDelayedOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, localParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
|
||||
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey)
|
||||
Transactions.addSigs(claimDelayed, sig)
|
||||
})
|
||||
|
||||
// those are the preimages to existing received htlcs
|
||||
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
|
||||
|
||||
val htlcTxes = localCommit.publishableTxs.htlcTxsAndSigs.collect {
|
||||
// incoming htlc for which we have the preimage: we spend it directly
|
||||
case HtlcTxAndSigs(txinfo@HtlcSuccessTx(_, _, paymentHash), localSig, remoteSig) if preimages.exists(r => sha256(r) == paymentHash) =>
|
||||
generateTx("htlc-success")(Try {
|
||||
val preimage = preimages.find(r => sha256(r) == paymentHash).get
|
||||
Transactions.addSigs(txinfo, localSig, remoteSig, preimage)
|
||||
})
|
||||
|
||||
// (incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back)
|
||||
|
||||
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
|
||||
case HtlcTxAndSigs(txinfo: HtlcTimeoutTx, localSig, remoteSig) =>
|
||||
generateTx("htlc-timeout")(Try {
|
||||
Transactions.addSigs(txinfo, localSig, remoteSig)
|
||||
})
|
||||
}.flatten
|
||||
|
||||
// all htlc output to us are delayed, so we need to claim them as soon as the delay is over
|
||||
val htlcDelayedTxes = htlcTxes.map {
|
||||
case txinfo: TransactionWithInputInfo => generateTx("claim-delayed-output")(Try {
|
||||
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
|
||||
val claimDelayed = Transactions.makeClaimDelayedOutputTx(txinfo.tx, Satoshi(localParams.dustLimitSatoshis), localRevocationPubkey, localParams.toSelfDelay, localDelayedPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed)
|
||||
val sig = Transactions.sign(claimDelayed, localDelayedPrivkey)
|
||||
Transactions.addSigs(claimDelayed, sig)
|
||||
})
|
||||
}.flatten
|
||||
|
||||
// OPTIONAL: let's check transactions are actually spendable
|
||||
//val txes = mainDelayedTx +: (htlcTxes ++ htlcDelayedTxes)
|
||||
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
|
||||
|
||||
LocalCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainDelayedOutputTx = mainDelayedTx.map(_.tx),
|
||||
htlcSuccessTxs = htlcTxes.collect { case c: HtlcSuccessTx => c.tx },
|
||||
htlcTimeoutTxs = htlcTxes.collect { case c: HtlcTimeoutTx => c.tx },
|
||||
claimHtlcDelayedTx = htlcDelayedTxes.map(_.tx),
|
||||
spent = Map.empty)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Claim all the HTLCs that we've received from their current commit tx
|
||||
*
|
||||
* @param commitments our commitment data, which include payment preimages
|
||||
* @return a list of transactions (one per HTLC that we can claim)
|
||||
*/
|
||||
def claimRemoteCommitTxOutputs(commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction): RemoteCommitPublished = {
|
||||
import commitments.{commitInput, localParams, remoteParams}
|
||||
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
|
||||
val (remoteCommitTx, htlcTimeoutTxs, htlcSuccessTxs) = Commitments.makeRemoteTxs(remoteCommit.index, localParams, remoteParams, commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec)
|
||||
require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx")
|
||||
|
||||
val localPaymentPrivkey = Generators.derivePrivKey(localParams.paymentKey, remoteCommit.remotePerCommitmentPoint)
|
||||
val localHtlcPrivkey = Generators.derivePrivKey(localParams.htlcKey, remoteCommit.remotePerCommitmentPoint)
|
||||
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
|
||||
val localPerCommitmentPoint = Generators.perCommitPoint(localParams.shaSeed, commitments.localCommit.index.toInt)
|
||||
val localRevocationPubKey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
|
||||
val remoteRevocationPubkey = Generators.revocationPubKey(localParams.revocationBasepoint, remoteCommit.remotePerCommitmentPoint)
|
||||
|
||||
// no need to use a high fee rate for our main output (we are the only one who can spend it)
|
||||
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
|
||||
// we need to use a rather high fee for htlc-claim because we compete with the counterparty
|
||||
val feeratePerKwHtlc = Globals.feeratesPerKw.get.block_1
|
||||
|
||||
// first we will claim our main output right away
|
||||
val mainTx = generateTx("claim-p2wpkh-output")(Try {
|
||||
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPaymentPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwMain)
|
||||
val sig = Transactions.sign(claimMain, localPaymentPrivkey)
|
||||
Transactions.addSigs(claimMain, localPaymentPrivkey.publicKey, sig)
|
||||
})
|
||||
|
||||
// those are the preimages to existing received htlcs
|
||||
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
|
||||
|
||||
// remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa
|
||||
val txes = commitments.remoteCommit.spec.htlcs.collect {
|
||||
// incoming htlc for which we have the preimage: we spend it directly
|
||||
case DirectedHtlc(OUT, add: UpdateAddHtlc) if preimages.exists(r => sha256(r) == add.paymentHash) => generateTx("claim-htlc-success")(Try {
|
||||
val preimage = preimages.find(r => sha256(r) == add.paymentHash).get
|
||||
val tx = Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
|
||||
val sig = Transactions.sign(tx, localHtlcPrivkey)
|
||||
Transactions.addSigs(tx, sig, preimage)
|
||||
})
|
||||
|
||||
// (incoming htlc for which we don't have the preimage: nothing to do, it will timeout eventually and they will get their funds back)
|
||||
|
||||
// outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout
|
||||
case DirectedHtlc(IN, add: UpdateAddHtlc) => generateTx("claim-htlc-timeout")(Try {
|
||||
val tx = Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, Satoshi(localParams.dustLimitSatoshis), localHtlcPrivkey.publicKey, remoteHtlcPubkey, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc)
|
||||
val sig = Transactions.sign(tx, localHtlcPrivkey)
|
||||
Transactions.addSigs(tx, sig)
|
||||
})
|
||||
}.toSeq.flatten
|
||||
|
||||
// OPTIONAL: let's check transactions are actually spendable
|
||||
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
|
||||
|
||||
RemoteCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainOutputTx = mainTx.map(_.tx),
|
||||
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
|
||||
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx },
|
||||
spent = Map.empty
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When an unexpected transaction spending the funding tx is detected:
|
||||
* 1) we find out if the published transaction is one of remote's revoked txs
|
||||
* 2) and then:
|
||||
* a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds
|
||||
* b) otherwise there is nothing we can do
|
||||
*
|
||||
* @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment
|
||||
*/
|
||||
def claimRevokedRemoteCommitTxOutputs(commitments: Commitments, tx: Transaction): Option[RevokedCommitPublished] = {
|
||||
import commitments._
|
||||
require(tx.txIn.size == 1, "commitment tx should have 1 input")
|
||||
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime)
|
||||
// this tx has been published by remote, so we need to invert local/remote params
|
||||
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localParams.paymentBasepoint)
|
||||
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
|
||||
logger.warn(s"counterparty has published revoked commit txnumber=$txnumber")
|
||||
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
|
||||
remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txnumber)
|
||||
.map(d => Scalar(d))
|
||||
.map { remotePerCommitmentSecret =>
|
||||
val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint
|
||||
|
||||
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
|
||||
val remoteRevocationPrivkey = Generators.revocationPrivKey(localParams.revocationSecret, remotePerCommitmentSecret)
|
||||
val localPrivkey = Generators.derivePrivKey(localParams.paymentKey, remotePerCommitmentPoint)
|
||||
|
||||
// no need to use a high fee rate for our main output (we are the only one who can spend it)
|
||||
val feeratePerKwMain = Globals.feeratesPerKw.get.blocks_6
|
||||
// we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty
|
||||
val feeratePerKwPenalty = Globals.feeratesPerKw.get.block_1
|
||||
|
||||
// first we will claim our main output right away
|
||||
val mainTx = generateTx("claim-p2wpkh-output")(Try {
|
||||
val claimMain = Transactions.makeClaimP2WPKHOutputTx(tx, Satoshi(localParams.dustLimitSatoshis), localPrivkey.publicKey, localParams.defaultFinalScriptPubKey, feeratePerKwMain)
|
||||
val sig = Transactions.sign(claimMain, localPrivkey)
|
||||
Transactions.addSigs(claimMain, localPrivkey.publicKey, sig)
|
||||
})
|
||||
|
||||
// then we punish them by stealing their main output
|
||||
val mainPenaltyTx = generateTx("main-penalty")(Try {
|
||||
// TODO: we should use the current fee rate, not the initial fee rate that we get from localParams
|
||||
val txinfo = Transactions.makeMainPenaltyTx(tx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPrivkey.publicKey, localParams.defaultFinalScriptPubKey, remoteParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty)
|
||||
val sig = Transactions.sign(txinfo, remoteRevocationPrivkey)
|
||||
Transactions.addSigs(txinfo, sig)
|
||||
})
|
||||
|
||||
// TODO: we don't claim htlcs outputs yet
|
||||
|
||||
// OPTIONAL: let's check transactions are actually spendable
|
||||
//val txes = mainDelayedRevokedTx :: Nil
|
||||
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
|
||||
|
||||
RevokedCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainOutputTx = mainTx.map(_.tx),
|
||||
mainPenaltyTx = mainPenaltyTx.map(_.tx),
|
||||
claimHtlcTimeoutTxs = Nil,
|
||||
htlcTimeoutTxs = Nil,
|
||||
htlcPenaltyTxs = Nil,
|
||||
spent = Map.empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
|
||||
* local commit scenario and keep track of it.
|
||||
*
|
||||
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
|
||||
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
|
||||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param localCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction) = {
|
||||
// even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate
|
||||
// over all of them to check if they are relevant
|
||||
val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint =>
|
||||
// is this the commit tx itself ? (we could do this outside of the loop...)
|
||||
val isCommitTx = localCommitPublished.commitTx.txid == tx.txid
|
||||
// does the tx spend an output of the local commitment tx?
|
||||
val spendsTheCommitTx = localCommitPublished.commitTx.txid == outPoint.txid
|
||||
// is the tx one of our 3rd stage delayed txes? (a 3rd stage tx is a tx spending the output of an htlc tx, which
|
||||
// is itself spending the output of the commitment tx)
|
||||
val is3rdStageDelayedTx = localCommitPublished.claimHtlcDelayedTx.map(_.txid).contains(outPoint.txid)
|
||||
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
localCommitPublished.copy(spent = localCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
|
||||
* remote commit scenario and keep track of it.
|
||||
*
|
||||
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
|
||||
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
|
||||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param remoteCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction) = {
|
||||
// even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate
|
||||
// over all of them to check if they are relevant
|
||||
val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint =>
|
||||
// is this the commit tx itself ? (we could do this outside of the loop...)
|
||||
val isCommitTx = remoteCommitPublished.commitTx.txid == tx.txid
|
||||
// does the tx spend an output of the local commitment tx?
|
||||
val spendsTheCommitTx = remoteCommitPublished.commitTx.txid == outPoint.txid
|
||||
// TODO: we don't currently spend htlc transactions
|
||||
isCommitTx || spendsTheCommitTx
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
remoteCommitPublished.copy(spent = remoteCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* In CLOSING state, when we are notified that a transaction has been confirmed, we check if this tx belongs in the
|
||||
* revoked commit scenario and keep track of it.
|
||||
*
|
||||
* We need to keep track of all transactions spending the outputs of the commitment tx, because some outputs can be
|
||||
* spent both by us and our counterparty. Because of that, some of our transactions may never confirm and we don't
|
||||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param revokedCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction) = {
|
||||
// even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate
|
||||
// over all of them to check if they are relevant
|
||||
val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint =>
|
||||
// is this the commit tx itself ? (we could do this outside of the loop...)
|
||||
val isCommitTx = revokedCommitPublished.commitTx.txid == tx.txid
|
||||
// does the tx spend an output of the local commitment tx?
|
||||
val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid
|
||||
isCommitTx || spendsTheCommitTx
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
revokedCommitPublished.copy(spent = revokedCommitPublished.spent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* A local commit is considered done when:
|
||||
* - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours)
|
||||
* - all 3rd stage txes (txes spending htlc txes) have been confirmed
|
||||
*
|
||||
* @param localCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def isLocalCommitDone(localCommitPublished: LocalCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
val isCommitTxConfirmed = localCommitPublished.spent.values.toSet.contains(localCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx? we just substract all known spent outputs from the ones we control
|
||||
val commitOutputsSpendableByUs = (localCommitPublished.claimMainDelayedOutputTx.toSeq ++ localCommitPublished.htlcSuccessTxs ++ localCommitPublished.htlcTimeoutTxs)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- localCommitPublished.spent.keys
|
||||
// which htlc delayed txes can we expect to be confirmed?
|
||||
val unconfirmedHtlcDelayedTxes = localCommitPublished.claimHtlcDelayedTx
|
||||
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- localCommitPublished.spent.values).isEmpty) // only the txes which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx)
|
||||
.filterNot(tx => localCommitPublished.spent.values.toSet.contains(tx.txid)) // has the tx already been confirmed?
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxes.isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed
|
||||
* (even if the spending tx was not ours).
|
||||
*
|
||||
* @param remoteCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def isRemoteCommitDone(remoteCommitPublished: RemoteCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
val isCommitTxConfirmed = remoteCommitPublished.spent.values.toSet.contains(remoteCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx?
|
||||
val commitOutputsSpendableByUs = (remoteCommitPublished.claimMainOutputTx.toSeq ++ remoteCommitPublished.claimHtlcSuccessTxs ++ remoteCommitPublished.claimHtlcTimeoutTxs)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- remoteCommitPublished.spent.keys
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed
|
||||
* (even if the spending tx was not ours).
|
||||
*
|
||||
* @param revokedCommitPublished
|
||||
* @return
|
||||
*/
|
||||
def isRevokedCommitDone(revokedCommitPublished: RevokedCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
val isCommitTxConfirmed = revokedCommitPublished.spent.values.toSet.contains(revokedCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx?
|
||||
val commitOutputsSpendableByUs = (revokedCommitPublished.claimMainOutputTx.toSeq ++ revokedCommitPublished.mainPenaltyTx)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- revokedCommitPublished.spent.keys
|
||||
// TODO: we don't currently spend htlc transactions
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,161 +0,0 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import org.spongycastle.util.encoders.Hex
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/**
|
||||
* Bit stream that can be written to and read at both ends (i.e. you can read from the end or the beginning of the stream)
|
||||
*
|
||||
* @param bytes bits packed as bytes, the last byte is padded with 0s
|
||||
* @param offstart offset at which the first bit is in the first byte
|
||||
* @param offend offset at which the last bit is in the last byte
|
||||
*/
|
||||
case class BitStream(bytes: Vector[Byte], offstart: Int, offend: Int) {
|
||||
|
||||
// offstart: 0 1 2 3 4 5 6 7
|
||||
// offend: 7 6 5 4 3 2 1 0
|
||||
import BitStream._
|
||||
|
||||
def bitCount = 8 * bytes.length - offstart - offend
|
||||
|
||||
def isEmpty = bitCount == 0
|
||||
|
||||
/**
|
||||
* append a byte to a bitstream
|
||||
*
|
||||
* @param input byte to append
|
||||
* @return an updated bitstream
|
||||
*/
|
||||
def writeByte(input: Byte): BitStream = offend match {
|
||||
case 0 => this.copy(bytes = this.bytes :+ input)
|
||||
case shift =>
|
||||
val input1 = input & 0xff
|
||||
val last = ((bytes.last | (input1 >>> (8 - shift))) & 0xff).toByte
|
||||
val next = ((input1 << shift) & 0xff).toByte
|
||||
this.copy(bytes = bytes.dropRight(1) ++ Vector(last, next))
|
||||
}
|
||||
|
||||
/**
|
||||
* append bytes to a bitstream
|
||||
*
|
||||
* @param input bytes to append
|
||||
* @return an udpdate bitstream
|
||||
*/
|
||||
def writeBytes(input: Seq[Byte]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeByte(b) }
|
||||
|
||||
/**
|
||||
* append a bit to a bistream
|
||||
*
|
||||
* @param bit bit to append
|
||||
* @return an update bitstream
|
||||
*/
|
||||
def writeBit(bit: Bit): BitStream = offend match {
|
||||
case 0 if bit =>
|
||||
BitStream(bytes :+ 0x80.toByte, offstart, 7)
|
||||
case 0 =>
|
||||
BitStream(bytes :+ 0x00.toByte, offstart, 7)
|
||||
case n if bit =>
|
||||
val last = (bytes.last + (1 << (offend - 1))).toByte
|
||||
BitStream(bytes.updated(bytes.length - 1, last), offstart, offend - 1)
|
||||
case n =>
|
||||
BitStream(bytes, offstart, offend - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* append bits to a bistream
|
||||
*
|
||||
* @param input bits to append
|
||||
* @return an update bitstream
|
||||
*/
|
||||
def writeBits(input: Seq[Bit]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeBit(b) }
|
||||
|
||||
/**
|
||||
* read the last bit from a bitstream
|
||||
*
|
||||
* @return a (stream, bit) pair where stream is an updated bitstream and bit is the last bit
|
||||
*/
|
||||
def popBit: (BitStream, Bit) = offend match {
|
||||
case 7 => BitStream(bytes.dropRight(1), offstart, 0) -> lastBit
|
||||
case n =>
|
||||
val shift = n + 1
|
||||
val last = (bytes.last >>> shift) << shift
|
||||
BitStream(bytes.updated(bytes.length - 1, last.toByte), offstart, offend + 1) -> lastBit
|
||||
}
|
||||
|
||||
/**
|
||||
* read the last byte from a bitstream
|
||||
*
|
||||
* @return a (stream, byte) pair where stream is an updated bitstream and byte is the last byte
|
||||
*/
|
||||
def popByte: (BitStream, Byte) = offend match {
|
||||
case 0 => BitStream(bytes.dropRight(1), offstart, offend) -> bytes.last
|
||||
case shift =>
|
||||
val a = bytes(bytes.length - 2) & 0xff
|
||||
val b = bytes(bytes.length - 1) & 0xff
|
||||
val byte = ((a << (8 - shift)) | (b >>> shift)) & 0xff
|
||||
val a1 = (a >>> shift) << shift
|
||||
BitStream(bytes.dropRight(2) :+ a1.toByte, offstart, offend) -> byte.toByte
|
||||
}
|
||||
|
||||
def popBytes(n: Int): (BitStream, Seq[Byte]) = {
|
||||
@tailrec
|
||||
def loop(stream: BitStream, acc: Seq[Byte]): (BitStream, Seq[Byte]) =
|
||||
if (acc.length == n) (stream, acc) else {
|
||||
val (stream1, value) = stream.popByte
|
||||
loop(stream1, acc :+ value)
|
||||
}
|
||||
|
||||
loop(this, Nil)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the first bit from a bitstream
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def readBit: (BitStream, Bit) = offstart match {
|
||||
case 7 => BitStream(bytes.tail, 0, offend) -> firstBit
|
||||
case _ => BitStream(bytes, offstart + 1, offend) -> firstBit
|
||||
}
|
||||
|
||||
def readBits(count: Int): (BitStream, Seq[Bit]) = {
|
||||
@tailrec
|
||||
def loop(stream: BitStream, acc: Seq[Bit]): (BitStream, Seq[Bit]) = if (acc.length == count) (stream, acc) else {
|
||||
val (stream1, bit) = stream.readBit
|
||||
loop(stream1, acc :+ bit)
|
||||
}
|
||||
|
||||
loop(this, Nil)
|
||||
}
|
||||
|
||||
/**
|
||||
* read the first byte from a bitstream
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def readByte: (BitStream, Byte) = {
|
||||
val byte = ((bytes(0) << offstart) | (bytes(1) >>> (7 - offstart))) & 0xff
|
||||
BitStream(bytes.tail, offstart, offend) -> byte.toByte
|
||||
}
|
||||
|
||||
def isSet(pos: Int): Boolean = {
|
||||
val pos1 = pos + offstart
|
||||
(bytes(pos1 / 8) & (1 << (7 - (pos1 % 8)))) != 0
|
||||
}
|
||||
|
||||
def firstBit = (bytes.head & (1 << (7 - offstart))) != 0
|
||||
|
||||
def lastBit = (bytes.last & (1 << offend)) != 0
|
||||
|
||||
def toBinString: String = "0b" + (for (i <- 0 until bitCount) yield if (isSet(i)) '1' else '0').mkString
|
||||
|
||||
def toHexString: String = "0x" + Hex.toHexString(bytes.toArray).toLowerCase
|
||||
}
|
||||
|
||||
object BitStream {
|
||||
type Bit = Boolean
|
||||
val Zero = false
|
||||
val One = true
|
||||
val empty = BitStream(Vector.empty[Byte], 0, 0)
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.HasCommitments
|
||||
|
||||
trait ChannelsDb {
|
||||
|
||||
def addOrUpdateChannel(state: HasCommitments)
|
||||
|
||||
def removeChannel(channelId: BinaryData)
|
||||
|
||||
def listChannels(): List[HasCommitments]
|
||||
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
|
||||
|
||||
trait NetworkDb {
|
||||
|
||||
def addNode(n: NodeAnnouncement)
|
||||
|
||||
def updateNode(n: NodeAnnouncement)
|
||||
|
||||
def removeNode(nodeId: PublicKey)
|
||||
|
||||
def listNodes(): List[NodeAnnouncement]
|
||||
|
||||
def addChannel(c: ChannelAnnouncement)
|
||||
|
||||
/**
|
||||
* This method removes 1 channel announcement and 2 channel updates (at both ends of the same channel)
|
||||
*
|
||||
* @param shortChannelId
|
||||
* @return
|
||||
*/
|
||||
def removeChannel(shortChannelId: Long)
|
||||
|
||||
def listChannels(): List[ChannelAnnouncement]
|
||||
|
||||
def addChannelUpdate(u: ChannelUpdate)
|
||||
|
||||
def updateChannelUpdate(u: ChannelUpdate)
|
||||
|
||||
def listChannelUpdates(): List[ChannelUpdate]
|
||||
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
|
||||
trait PeersDb {
|
||||
|
||||
def addOrUpdatePeer(nodeId: PublicKey, address: InetSocketAddress)
|
||||
|
||||
def removePeer(nodeId: PublicKey)
|
||||
|
||||
def listPeers(): List[(PublicKey, InetSocketAddress)]
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
|
||||
/**
|
||||
* This database stores the preimages that we have received from downstream
|
||||
* (either directly via UpdateFulfillHtlc or by extracting the value from the
|
||||
* blockchain).
|
||||
*
|
||||
* This means that this database is only used in the context of *relaying* payments.
|
||||
*
|
||||
* We need to be sure that if downstream is able to pulls funds from us, we can always
|
||||
* do the same from upstream, otherwise we lose money. Hence the need for persistence
|
||||
* to handle all corner cases.
|
||||
*
|
||||
*/
|
||||
trait PreimagesDb {
|
||||
|
||||
def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData)
|
||||
|
||||
def removePreimage(channelId: BinaryData, htlcId: Long)
|
||||
|
||||
def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)]
|
||||
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.HasCommitments
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
|
||||
|
||||
class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
||||
|
||||
import SqliteUtils._
|
||||
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
|
||||
}
|
||||
|
||||
override def addOrUpdateChannel(state: HasCommitments): Unit = {
|
||||
val data = stateDataCodec.encode(state).require.toByteArray
|
||||
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 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()
|
||||
}
|
||||
|
||||
override def listChannels(): List[HasCommitments] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM local_channels")
|
||||
codecList(rs, stateDataCodec)
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
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}
|
||||
|
||||
class SqliteNetworkDb(sqlite: Connection) extends NetworkDb {
|
||||
|
||||
import SqliteUtils._
|
||||
|
||||
{
|
||||
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, 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)")
|
||||
}
|
||||
|
||||
override def addNode(n: NodeAnnouncement): Unit = {
|
||||
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 = {
|
||||
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 = {
|
||||
val statement = sqlite.prepareStatement("DELETE FROM nodes WHERE node_id=?")
|
||||
statement.setBytes(1, nodeId.toBin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listNodes(): List[NodeAnnouncement] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM nodes")
|
||||
codecList(rs, nodeAnnouncementCodec)
|
||||
}
|
||||
|
||||
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 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(): List[ChannelAnnouncement] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channels")
|
||||
codecList(rs, channelAnnouncementCodec)
|
||||
}
|
||||
|
||||
override def addChannelUpdate(u: ChannelUpdate): Unit = {
|
||||
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 = {
|
||||
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(): List[ChannelUpdate] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT data FROM channel_updates")
|
||||
codecList(rs, channelUpdateCodec)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
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.wire.LightningMessageCodecs.socketaddress
|
||||
import scodec.bits.BitVector
|
||||
|
||||
class SqlitePeersDb(sqlite: Connection) extends PeersDb {
|
||||
|
||||
{
|
||||
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, 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 removePeer(nodeId: Crypto.PublicKey): Unit = {
|
||||
val statement = sqlite.prepareStatement("DELETE FROM peers WHERE node_id=?")
|
||||
statement.setBytes(1, nodeId.toBin)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listPeers(): List[(PublicKey, InetSocketAddress)] = {
|
||||
val rs = sqlite.createStatement.executeQuery("SELECT node_id, data FROM peers")
|
||||
var l: List[(PublicKey, InetSocketAddress)] = Nil
|
||||
while (rs.next()) {
|
||||
l = l :+ (PublicKey(rs.getBytes("node_id")), socketaddress.decode(BitVector(rs.getBytes("data"))).require.value)
|
||||
}
|
||||
l
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.db.PreimagesDb
|
||||
|
||||
class SqlitePreimagesDb(sqlite: Connection) extends PreimagesDb {
|
||||
|
||||
{
|
||||
val statement = sqlite.createStatement
|
||||
// note: should we use a foreign key to local_channels table here?
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS preimages (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, preimage BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))")
|
||||
}
|
||||
|
||||
override def addPreimage(channelId: BinaryData, htlcId: Long, paymentPreimage: BinaryData): Unit = {
|
||||
val statement = sqlite.prepareStatement("INSERT OR IGNORE INTO preimages VALUES (?, ?, ?)")
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.setBytes(3, paymentPreimage)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def removePreimage(channelId: BinaryData, htlcId: Long): Unit = {
|
||||
val statement = sqlite.prepareStatement("DELETE FROM preimages WHERE channel_id=? AND htlc_id=?")
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, htlcId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def listPreimages(channelId: BinaryData): List[(BinaryData, Long, BinaryData)] = {
|
||||
val statement = sqlite.prepareStatement("SELECT htlc_id, preimage FROM preimages WHERE channel_id=?")
|
||||
statement.setBytes(1, channelId)
|
||||
val rs = statement.executeQuery()
|
||||
var l: List[(BinaryData, Long, BinaryData)] = Nil
|
||||
while (rs.next()) {
|
||||
l = l :+ (channelId, rs.getLong("htlc_id"), BinaryData(rs.getBytes("preimage")))
|
||||
}
|
||||
l
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package fr.acinq.eclair.db.sqlite
|
||||
|
||||
import java.sql.ResultSet
|
||||
|
||||
import scodec.Codec
|
||||
import scodec.bits.BitVector
|
||||
|
||||
object SqliteUtils {
|
||||
|
||||
/**
|
||||
* This helper assumes that there is a "data" column available, decodable with the provided codec
|
||||
*
|
||||
* TODO: we should use an scala.Iterator instead
|
||||
*
|
||||
* @param rs
|
||||
* @param codec
|
||||
* @tparam T
|
||||
* @return
|
||||
*/
|
||||
def codecList[T](rs: ResultSet, codec: Codec[T]): List[T] = {
|
||||
var l: List[T] = Nil
|
||||
while (rs.next()) {
|
||||
l = l :+ codec.decode(BitVector(rs.getBytes("data"))).require.value
|
||||
}
|
||||
l
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package fr.acinq.eclair.io
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, PoisonPill}
|
||||
import akka.io.Tcp
|
||||
import akka.util.ByteString
|
||||
|
||||
|
||||
/**
|
||||
* This implements an ACK-based throttling mechanism
|
||||
* See https://doc.akka.io/docs/akka/snapshot/scala/io-tcp.html#throttling-reads-and-writes
|
||||
*/
|
||||
class WriteAckSender(connection: ActorRef) extends Actor with ActorLogging {
|
||||
|
||||
// Note: this actor should be killed if connection dies
|
||||
|
||||
case object Ack extends Tcp.Event
|
||||
|
||||
override def receive = idle
|
||||
|
||||
def idle: Receive = {
|
||||
case data: ByteString =>
|
||||
connection ! Tcp.Write(data, Ack)
|
||||
context become buffering(Vector.empty[ByteString])
|
||||
}
|
||||
|
||||
def buffering(buffer: Vector[ByteString]): Receive = {
|
||||
case _: ByteString if buffer.size > MAX_BUFFERED =>
|
||||
log.warning(s"buffer overrun, closing connection")
|
||||
connection ! PoisonPill
|
||||
case data: ByteString =>
|
||||
log.debug(s"buffering write $data")
|
||||
context become buffering(buffer :+ data)
|
||||
case Ack =>
|
||||
buffer.headOption match {
|
||||
case Some(data) =>
|
||||
connection ! Tcp.Write(data, Ack)
|
||||
context become buffering(buffer.drop(1))
|
||||
case None =>
|
||||
log.debug(s"got last ack, back to idle")
|
||||
context become idle
|
||||
}
|
||||
}
|
||||
|
||||
override def unhandled(message: Any): Unit = log.warning(s"unhandled message $message")
|
||||
|
||||
val MAX_BUFFERED = 100000L
|
||||
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props, Status}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{NodeParams, randomBytes}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Created by PM on 17/06/2016.
|
||||
*/
|
||||
class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging {
|
||||
|
||||
override def receive: Receive = run(Map())
|
||||
|
||||
def run(h2r: Map[BinaryData, (BinaryData, PaymentRequest)]): Receive = {
|
||||
|
||||
case ReceivePayment(amount, desc) =>
|
||||
Try {
|
||||
val paymentPreimage = randomBytes(32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
(paymentPreimage, paymentHash, PaymentRequest(nodeParams.chainHash, Some(amount), paymentHash, nodeParams.privateKey, desc))
|
||||
} match {
|
||||
case Success((r, h, pr)) =>
|
||||
log.debug(s"generated payment request=${PaymentRequest.write(pr)} from amount=$amount")
|
||||
sender ! pr
|
||||
context.become(run(h2r + (h -> (r, pr))))
|
||||
case Failure(t) =>
|
||||
sender ! Status.Failure(t)
|
||||
}
|
||||
|
||||
case htlc: UpdateAddHtlc =>
|
||||
if (h2r.contains(htlc.paymentHash)) {
|
||||
val r = h2r(htlc.paymentHash)._1
|
||||
val pr = h2r(htlc.paymentHash)._2
|
||||
// The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however
|
||||
// it must not be greater than two times the requested amount.
|
||||
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#failure-messages
|
||||
pr.amount match {
|
||||
case Some(amount) if MilliSatoshi(htlc.amountMsat) < amount => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
|
||||
case Some(amount) if MilliSatoshi(htlc.amountMsat) > amount * 2 => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
|
||||
case _ =>
|
||||
log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}")
|
||||
// amount is correct or was not specified in the payment request
|
||||
sender ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
|
||||
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash))
|
||||
context.become(run(h2r - htlc.paymentHash))
|
||||
}
|
||||
} else {
|
||||
sender ! CMD_FAIL_HTLC(htlc.id, Right(UnknownPaymentHash), commit = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LocalPaymentHandler {
|
||||
def props(nodeParams: NodeParams) = Props(new LocalPaymentHandler(nodeParams))
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.wire.ChannelUpdate
|
||||
|
||||
|
||||
object PaymentHop {
|
||||
/**
|
||||
*
|
||||
* @param baseMsat fixed fee
|
||||
* @param proportional proportional fee
|
||||
* @param msat amount in millisatoshi
|
||||
* @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis
|
||||
*/
|
||||
def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000
|
||||
|
||||
/**
|
||||
*
|
||||
* @param reversePath sequence of Hops from recipient to a start of assisted path
|
||||
* @param msat an amount to send to a payment recipient
|
||||
* @return a sequence of extra hops with a pre-calculated fee for a given msat amount
|
||||
*/
|
||||
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = (List.empty[ExtraHop] /: reversePath) {
|
||||
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat), hop.cltvExpiryDelta) :: Nil
|
||||
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat + head.fee), hop.cltvExpiryDelta) :: head :: rest
|
||||
}
|
||||
}
|
||||
|
||||
trait PaymentHop {
|
||||
def nextFee(msat: Long): Long
|
||||
|
||||
def shortChannelId: Long
|
||||
|
||||
def cltvExpiryDelta: Int
|
||||
|
||||
def nodeId: PublicKey
|
||||
}
|
||||
|
||||
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop {
|
||||
def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat)
|
||||
|
||||
def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta
|
||||
|
||||
def shortChannelId: Long = lastUpdate.shortChannelId
|
||||
}
|
||||
@ -1,201 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.{ActorRef, FSM, LoggingFSM, Props, Status}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet}
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire._
|
||||
import scodec.Attempt
|
||||
|
||||
// @formatter:off
|
||||
case class ReceivePayment(amountMsat: MilliSatoshi, description: String)
|
||||
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5)
|
||||
|
||||
sealed trait PaymentResult
|
||||
case class PaymentSucceeded(route: Seq[Hop], paymentPreimage: BinaryData) extends PaymentResult
|
||||
sealed trait PaymentFailure
|
||||
case class LocalFailure(t: Throwable) extends PaymentFailure
|
||||
case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure
|
||||
case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure
|
||||
case class PaymentFailed(paymentHash: BinaryData, failures: Seq[PaymentFailure]) extends PaymentResult
|
||||
|
||||
sealed trait Data
|
||||
case object WaitingForRequest extends Data
|
||||
case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure]) extends Data
|
||||
case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(BinaryData, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long], hops: Seq[Hop]) extends Data
|
||||
|
||||
sealed trait State
|
||||
case object WAITING_FOR_REQUEST extends State
|
||||
case object WAITING_FOR_ROUTE extends State
|
||||
case object WAITING_FOR_PAYMENT_COMPLETE extends State
|
||||
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Created by PM on 26/08/2016.
|
||||
*/
|
||||
class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends LoggingFSM[State, Data] {
|
||||
|
||||
import PaymentLifecycle._
|
||||
|
||||
startWith(WAITING_FOR_REQUEST, WaitingForRequest)
|
||||
|
||||
when(WAITING_FOR_REQUEST) {
|
||||
case Event(c: SendPayment, WaitingForRequest) =>
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil)
|
||||
}
|
||||
|
||||
when(WAITING_FOR_ROUTE) {
|
||||
case Event(RouteResponse(hops, ignoreNodes, ignoreChannels), WaitingForRoute(s, c, failures)) =>
|
||||
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId.toHexString).mkString("->")}")
|
||||
val firstHop = hops.head
|
||||
val finalExpiry = Globals.blockCount.get().toInt + c.minFinalCltvExpiry.toInt
|
||||
val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops)
|
||||
// TODO: HACK!!!! see Router.scala (we actually store the first node id in the sig)
|
||||
if (firstHop.lastUpdate.signature.size == 32) {
|
||||
register ! Register.Forward(firstHop.lastUpdate.signature, cmd)
|
||||
} else {
|
||||
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
|
||||
}
|
||||
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)
|
||||
|
||||
case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) =>
|
||||
s ! PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(t))
|
||||
stop(FSM.Normal)
|
||||
}
|
||||
|
||||
when(WAITING_FOR_PAYMENT_COMPLETE) {
|
||||
case Event("ok", _) => stay()
|
||||
|
||||
case Event(fulfill: UpdateFulfillHtlc, w: WaitingForComplete) =>
|
||||
w.sender ! PaymentSucceeded(w.hops, fulfill.paymentPreimage)
|
||||
context.system.eventStream.publish(PaymentSent(MilliSatoshi(w.c.amountMsat), MilliSatoshi(w.cmd.amountMsat - w.c.amountMsat), w.cmd.paymentHash))
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>
|
||||
Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match {
|
||||
case None =>
|
||||
log.warning(s"cannot parse returned error ${fail.reason}")
|
||||
s ! PaymentFailed(c.paymentHash, failures = failures :+ UnreadableRemoteFailure(hops))
|
||||
stop(FSM.Normal)
|
||||
case Some(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId =>
|
||||
log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)")
|
||||
s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))
|
||||
stop(FSM.Normal)
|
||||
case Some(e@ErrorPacket(nodeId, failureMessage)) if failures.size + 1 >= c.maxAttempts =>
|
||||
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
|
||||
log.warning(s"too many failed attempts, failing the payment")
|
||||
s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))
|
||||
stop(FSM.Normal)
|
||||
case Some(e@ErrorPacket(nodeId, failureMessage: Node)) =>
|
||||
log.info(s"received an error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)")
|
||||
// let's try to route around this node
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
case Some(e@ErrorPacket(nodeId, failureMessage: Update)) =>
|
||||
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
|
||||
if (Announcements.checkSig(failureMessage.update, nodeId)) {
|
||||
// note that we check the sig, but we don't make sure that this update was for the exact channel we required
|
||||
// the reason is that we don't want to prevent relaying nodes to use another channel to the same N+1 node if they deem necessary
|
||||
failureMessage match {
|
||||
case _: TemporaryChannelFailure =>
|
||||
// node indicates that its outgoing channel is experiencing a transient issue (eg. channel capacity reached, too many in-flight htlc)
|
||||
hops.find(_.nodeId == nodeId).map(_.lastUpdate) match {
|
||||
case Some(u) if u.copy(signature = BinaryData.empty, timestamp = 0) == failureMessage.update.copy(signature = BinaryData.empty, timestamp = 0) =>
|
||||
// node returned the exact same update we used: in that case, let's temporarily exclude the channel from future routes, giving it time to recover
|
||||
val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId
|
||||
router ! ExcludeChannel(ChannelDesc(failureMessage.update.shortChannelId, nodeId, nextNodeId))
|
||||
case _ => // node returned a different update, maybe the payment will go through next time...
|
||||
}
|
||||
case _ => {}
|
||||
}
|
||||
// in any case, we forward the update to the router
|
||||
router ! failureMessage.update
|
||||
// let's try again, router will have updated its state
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels)
|
||||
} else {
|
||||
// this node is fishy, it gave us a bad sig!! let's filter it out
|
||||
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels)
|
||||
}
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
case Some(e@ErrorPacket(nodeId, failureMessage)) =>
|
||||
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
|
||||
// let's try again without the channel outgoing from nodeId
|
||||
val faultyChannel = hops.find(_.nodeId == nodeId).map(_.lastUpdate.shortChannelId)
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels ++ faultyChannel.toSet)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
}
|
||||
|
||||
case Event(fail: UpdateFailMalformedHtlc, _) =>
|
||||
log.info(s"first node in the route couldn't parse our htlc: fail=$fail")
|
||||
// this is a corner case, that can only happen when the *first* node in the route cannot parse the onion
|
||||
// (if this happens higher up in the route, the error would be wrapped in an UpdateFailHtlc and handled above)
|
||||
// let's consider it a local error and treat is as such
|
||||
self ! Status.Failure(new RuntimeException("first hop returned an UpdateFailMalformedHtlc message"))
|
||||
stay
|
||||
|
||||
case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) =>
|
||||
if (failures.size + 1 >= c.maxAttempts) {
|
||||
s ! PaymentFailed(c.paymentHash, failures :+ LocalFailure(t))
|
||||
stop(FSM.Normal)
|
||||
} else {
|
||||
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
|
||||
router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes, ignoreChannels + hops.head.lastUpdate.shortChannelId)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
object PaymentLifecycle {
|
||||
|
||||
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register)
|
||||
|
||||
def buildOnion(nodes: Seq[PublicKey], payloads: Seq[PerHopPayload], associatedData: BinaryData): Sphinx.PacketAndSecrets = {
|
||||
require(nodes.size == payloads.size)
|
||||
val sessionKey = randomKey
|
||||
val payloadsbin: Seq[BinaryData] = payloads
|
||||
.map(LightningMessageCodecs.perHopPayloadCodec.encode(_))
|
||||
.map {
|
||||
case Attempt.Successful(bitVector) => BinaryData(bitVector.toByteArray)
|
||||
case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause")
|
||||
}
|
||||
Sphinx.makePacket(sessionKey, nodes, payloadsbin, associatedData)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param finalAmountMsat the final htlc amount in millisatoshis
|
||||
* @param finalExpiry the final htlc expiry in number of blocks
|
||||
* @param hops the hops as computed by the router + extra routes from payment request
|
||||
* @return a (firstAmountMsat, firstExpiry, payloads) tuple where:
|
||||
* - firstAmountMsat is the amount for the first htlc in the route
|
||||
* - firstExpiry is the cltv expiry for the first htlc in the route
|
||||
* - a sequence of payloads that will be used to build the onion
|
||||
*/
|
||||
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[PaymentHop]): (Long, Int, Seq[PerHopPayload]) =
|
||||
hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)) {
|
||||
case ((msat, expiry, payloads), hop) =>
|
||||
(msat + hop.nextFee(msat), expiry + hop.cltvExpiryDelta, PerHopPayload(hop.shortChannelId, msat, expiry) +: payloads)
|
||||
}
|
||||
|
||||
// this is defined in BOLT 11
|
||||
val defaultMinFinalCltvExpiry = 9
|
||||
|
||||
def buildCommand(finalAmountMsat: Long, finalExpiry: Int, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
|
||||
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
|
||||
val nodes = hops.map(_.nextNodeId)
|
||||
// BOLT 2 requires that associatedData == paymentHash
|
||||
val onion = buildOnion(nodes, payloads, paymentHash)
|
||||
CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = None, commit = true) -> onion.sharedSecrets
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,513 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Bech32.Int5
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, _}
|
||||
import fr.acinq.eclair.crypto.BitStream
|
||||
import fr.acinq.eclair.crypto.BitStream.Bit
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{Amount, RoutingInfoTag, Timestamp}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Lightning Payment Request
|
||||
* see https://github.com/lightningnetwork/lightning-rfc/pull/183
|
||||
*
|
||||
* @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet
|
||||
* @param amount amount to pay (empty string means no amount is specified)
|
||||
* @param timestamp request timestamp (UNIX format)
|
||||
* @param nodeId id of the node emitting the payment request
|
||||
* @param tags payment tags; must include a single PaymentHash tag
|
||||
* @param signature request signature that will be checked against node id
|
||||
*/
|
||||
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.Tag], signature: BinaryData) {
|
||||
|
||||
amount.map(a => require(a > MilliSatoshi(0) && a <= PaymentRequest.maxAmount, s"amount is not valid"))
|
||||
require(tags.collect { case _: PaymentRequest.PaymentHashTag => {} }.size == 1, "there must be exactly one payment hash tag")
|
||||
require(tags.collect { case PaymentRequest.DescriptionTag(_) | PaymentRequest.DescriptionHashTag(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the payment hash
|
||||
*/
|
||||
def paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHashTag => p }.get.hash
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the description of the payment, or its hash
|
||||
*/
|
||||
def description: Either[String, BinaryData] = tags.collectFirst {
|
||||
case PaymentRequest.DescriptionTag(d) => Left(d)
|
||||
case PaymentRequest.DescriptionHashTag(h) => Right(h)
|
||||
}.get
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the fallback address if any. It could be a script address, pubkey address, ..
|
||||
*/
|
||||
def fallbackAddress(): Option[String] = tags.collectFirst {
|
||||
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, hash)
|
||||
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, hash)
|
||||
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, hash)
|
||||
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
|
||||
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, hash)
|
||||
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, hash)
|
||||
}
|
||||
|
||||
def routingInfo(): Seq[RoutingInfoTag] = tags.collect { case t: RoutingInfoTag => t }
|
||||
|
||||
def expiry: Option[Long] = tags.collectFirst {
|
||||
case PaymentRequest.ExpiryTag(seconds) => seconds
|
||||
}
|
||||
|
||||
def minFinalCltvExpiry: Option[Long] = tags.collectFirst {
|
||||
case PaymentRequest.MinFinalCltvExpiryTag(expiry) => expiry
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return a representation of this payment request, without its signature, as a bit stream. This is what will be signed.
|
||||
*/
|
||||
def stream: BitStream = {
|
||||
val stream = BitStream.empty
|
||||
val int5s = Timestamp.encode(timestamp) ++ (tags.map(_.toInt5s).flatten)
|
||||
val stream1 = int5s.foldLeft(stream)(PaymentRequest.write5)
|
||||
stream1
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the hash of this payment request
|
||||
*/
|
||||
def hash: BinaryData = Crypto.sha256(s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8") ++ stream.bytes)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param priv private key
|
||||
* @return a signed payment request
|
||||
*/
|
||||
def sign(priv: PrivateKey): PaymentRequest = {
|
||||
val (r, s) = Crypto.sign(hash, priv)
|
||||
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), hash)
|
||||
val recid = if (nodeId == pub1) 0.toByte else 1.toByte
|
||||
val signature = PaymentRequest.Signature.encode(r, s, recid)
|
||||
this.copy(signature = signature)
|
||||
}
|
||||
}
|
||||
|
||||
object PaymentRequest {
|
||||
|
||||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc
|
||||
val maxAmount = MilliSatoshi(4294967296L)
|
||||
|
||||
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey,
|
||||
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
|
||||
extraHops: Seq[Seq[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = {
|
||||
|
||||
val prefix = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash => "lntb"
|
||||
case Block.TestnetGenesisBlock.hash => "lntb"
|
||||
case Block.LivenetGenesisBlock.hash => "lnbc"
|
||||
}
|
||||
|
||||
PaymentRequest(
|
||||
prefix = prefix,
|
||||
amount = amount,
|
||||
timestamp = timestamp,
|
||||
nodeId = privateKey.publicKey,
|
||||
tags = List(
|
||||
Some(PaymentHashTag(paymentHash)),
|
||||
Some(DescriptionTag(description)),
|
||||
expirySeconds.map(ExpiryTag(_))
|
||||
).flatten ++ extraHops.map(RoutingInfoTag(_)),
|
||||
signature = BinaryData.empty)
|
||||
.sign(privateKey)
|
||||
}
|
||||
|
||||
sealed trait Tag {
|
||||
def toInt5s: Seq[Int5]
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment Hash Tag
|
||||
*
|
||||
* @param hash payment hash
|
||||
*/
|
||||
case class PaymentHashTag(hash: BinaryData) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = Bech32.eight2five(hash)
|
||||
Seq(Bech32.map('p'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description Tag
|
||||
*
|
||||
* @param description a free-format string that will be included in the payment request
|
||||
*/
|
||||
case class DescriptionTag(description: String) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = Bech32.eight2five(description.getBytes("UTF-8"))
|
||||
Seq(Bech32.map('d'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash Tag
|
||||
*
|
||||
* @param hash hash that will be included in the payment request, and can be checked against the hash of a
|
||||
* long description, an invoice, ...
|
||||
*/
|
||||
case class DescriptionHashTag(hash: BinaryData) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = Bech32.eight2five(hash)
|
||||
Seq(Bech32.map('h'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fallback Payment Tag that specifies a fallback payment address to be used if LN payment cannot be processed
|
||||
*
|
||||
* @param version address version; valid values are
|
||||
* - 17 (pubkey hash)
|
||||
* - 18 (script hash)
|
||||
* - 0 (segwit hash: p2wpkh (20 bytes) or p2wsh (32 bytes))
|
||||
* @param hash address hash
|
||||
*/
|
||||
case class FallbackAddressTag(version: Byte, hash: BinaryData) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = version +: Bech32.eight2five(hash)
|
||||
Seq(Bech32.map('f'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
|
||||
}
|
||||
}
|
||||
|
||||
object FallbackAddressTag {
|
||||
/**
|
||||
*
|
||||
* @param address valid base58 or bech32 address
|
||||
* @return a FallbackAddressTag instance
|
||||
*/
|
||||
def apply(address: String): FallbackAddressTag = {
|
||||
Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get
|
||||
}
|
||||
|
||||
def fromBase58Address(address: String): FallbackAddressTag = {
|
||||
val (prefix, hash) = Base58Check.decode(address)
|
||||
prefix match {
|
||||
case Base58.Prefix.PubkeyAddress => FallbackAddressTag(17, hash)
|
||||
case Base58.Prefix.PubkeyAddressTestnet => FallbackAddressTag(17, hash)
|
||||
case Base58.Prefix.ScriptAddress => FallbackAddressTag(18, hash)
|
||||
case Base58.Prefix.ScriptAddressTestnet => FallbackAddressTag(18, hash)
|
||||
}
|
||||
}
|
||||
|
||||
def fromBech32Address(address: String): FallbackAddressTag = {
|
||||
val (prefix, hash) = Bech32.decodeWitnessAddress(address)
|
||||
FallbackAddressTag(prefix, hash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra hop contained in RoutingInfoTag
|
||||
*
|
||||
* @param nodeId node id
|
||||
* @param shortChannelId channel id
|
||||
* @param fee node fee
|
||||
* @param cltvExpiryDelta node cltv expiry delta
|
||||
*/
|
||||
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) extends PaymentHop {
|
||||
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
|
||||
Protocol.writeUInt64(fee, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
|
||||
|
||||
// Fee is already pre-calculated for extra hops
|
||||
def nextFee(msat: Long): Long = fee
|
||||
}
|
||||
|
||||
/**
|
||||
* Routing Info Tag
|
||||
*
|
||||
* @param path one or more entries containing extra routing information for a private route
|
||||
*/
|
||||
case class RoutingInfoTag(path: Seq[ExtraHop]) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = Bech32.eight2five(path.flatMap(_.pack))
|
||||
Seq(Bech32.map('r'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
|
||||
}
|
||||
}
|
||||
|
||||
object RoutingInfoTag {
|
||||
def parse(data: Seq[Byte]) = {
|
||||
val pubkey = data.slice(0, 33)
|
||||
val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN)
|
||||
val fee = Protocol.uint64(data.slice(33 + 8, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
|
||||
val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN)
|
||||
ExtraHop(PublicKey(pubkey), shortChannelId, fee, cltv)
|
||||
}
|
||||
|
||||
def parseAll(data: Seq[Byte]): Seq[ExtraHop] =
|
||||
data.grouped(chunkLength).map(parse).toList
|
||||
|
||||
val chunkLength: Int = 33 + 8 + 8 + 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Expiry Date
|
||||
*
|
||||
* @param seconds expiry data for this payment request
|
||||
*/
|
||||
case class ExpiryTag(seconds: Long) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = writeUnsignedLong(seconds)
|
||||
Bech32.map('x') +: (writeSize(ints.size) ++ ints)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Min final CLTV expiry
|
||||
*
|
||||
* @param blocks min final cltv expiry, in blocks
|
||||
*/
|
||||
case class MinFinalCltvExpiryTag(blocks: Long) extends Tag {
|
||||
override def toInt5s = {
|
||||
val ints = writeUnsignedLong(blocks)
|
||||
Bech32.map('c') +: (writeSize(ints.size) ++ ints)
|
||||
}
|
||||
}
|
||||
|
||||
object Amount {
|
||||
|
||||
/**
|
||||
* @param amount
|
||||
* @return the unit allowing for the shortest representation possible
|
||||
*/
|
||||
def unit(amount: MilliSatoshi): Char = amount.amount * 10 match { // 1 milli-satoshis == 10 pico-bitcoin
|
||||
case pico if pico % 1000 > 0 => 'p'
|
||||
case pico if pico % 1000000 > 0 => 'n'
|
||||
case pico if pico % 1000000000 > 0 => 'u'
|
||||
case _ => 'm'
|
||||
}
|
||||
|
||||
def decode(input: String): Option[MilliSatoshi] =
|
||||
input match {
|
||||
case "" => None
|
||||
case a if a.last == 'p' => Some(MilliSatoshi(a.dropRight(1).toLong / 10L)) // 1 pico-bitcoin == 10 milli-satoshis
|
||||
case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L))
|
||||
case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L))
|
||||
case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L))
|
||||
}
|
||||
|
||||
def encode(amount: Option[MilliSatoshi]): String = {
|
||||
amount match {
|
||||
case None => ""
|
||||
case Some(amt) if unit(amt) == 'p' => s"${amt.amount * 10L}p" // 1 pico-bitcoin == 10 milli-satoshis
|
||||
case Some(amt) if unit(amt) == 'n' => s"${amt.amount / 100L}n"
|
||||
case Some(amt) if unit(amt) == 'u' => s"${amt.amount / 100000L}u"
|
||||
case Some(amt) if unit(amt) == 'm' => s"${amt.amount / 100000000L}m"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Tag {
|
||||
def parse(input: Seq[Byte]): Tag = {
|
||||
val tag = input(0)
|
||||
val len = input(1) * 32 + input(2)
|
||||
tag match {
|
||||
case p if p == Bech32.map('p') =>
|
||||
val hash = Bech32.five2eight(input.drop(3).take(52))
|
||||
PaymentHashTag(hash)
|
||||
case d if d == Bech32.map('d') =>
|
||||
val description = new String(Bech32.five2eight(input.drop(3).take(len)).toArray, "UTF-8")
|
||||
DescriptionTag(description)
|
||||
case h if h == Bech32.map('h') =>
|
||||
val hash: BinaryData = Bech32.five2eight(input.drop(3).take(len))
|
||||
DescriptionHashTag(hash)
|
||||
case f if f == Bech32.map('f') =>
|
||||
val version = input(3)
|
||||
val prog = Bech32.five2eight(input.drop(4).take(len - 1))
|
||||
version match {
|
||||
case v if v >= 0 && v <= 16 =>
|
||||
FallbackAddressTag(version, prog)
|
||||
case 17 | 18 =>
|
||||
FallbackAddressTag(version, prog)
|
||||
}
|
||||
case r if r == Bech32.map('r') =>
|
||||
val data = Bech32.five2eight(input.drop(3).take(len))
|
||||
val path = RoutingInfoTag.parseAll(data)
|
||||
RoutingInfoTag(path)
|
||||
case x if x == Bech32.map('x') =>
|
||||
val expiry = readUnsignedLong(len, input.drop(3).take(len))
|
||||
ExpiryTag(expiry)
|
||||
case c if c == Bech32.map('c') =>
|
||||
val expiry = readUnsignedLong(len, input.drop(3).take(len))
|
||||
MinFinalCltvExpiryTag(expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Timestamp {
|
||||
def decode(data: Seq[Int5]): Long = data.take(7).foldLeft(0L)((a, b) => a * 32 + b)
|
||||
|
||||
def encode(timestamp: Long, acc: Seq[Int5] = Nil): Seq[Int5] = if (acc.length == 7) acc else {
|
||||
encode(timestamp / 32, (timestamp % 32).toByte +: acc)
|
||||
}
|
||||
}
|
||||
|
||||
object Signature {
|
||||
/**
|
||||
*
|
||||
* @param signature 65-bytes signatyre: r (32 bytes) | s (32 bytes) | recid (1 bytes)
|
||||
* @return a (r, s, recoveryId)
|
||||
*/
|
||||
def decode(signature: BinaryData): (BigInteger, BigInteger, Byte) = {
|
||||
require(signature.length == 65)
|
||||
val r = new BigInteger(1, signature.take(32).toArray)
|
||||
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
|
||||
val recid = signature.last
|
||||
(r, s, recid)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return a 65 bytes representation of (r, s, recid)
|
||||
*/
|
||||
def encode(r: BigInteger, s: BigInteger, recid: Byte): BinaryData = {
|
||||
Crypto.fixSize(r.toByteArray.dropWhile(_ == 0.toByte)) ++ Crypto.fixSize(s.toByteArray.dropWhile(_ == 0.toByte)) :+ recid
|
||||
}
|
||||
}
|
||||
|
||||
def toBits(value: Int5): Seq[Bit] = Seq((value & 16) != 0, (value & 8) != 0, (value & 4) != 0, (value & 2) != 0, (value & 1) != 0)
|
||||
|
||||
/**
|
||||
* write a 5bits integer to a stream
|
||||
*
|
||||
* @param stream stream to write to
|
||||
* @param value a 5bits value
|
||||
* @return an upated stream
|
||||
*/
|
||||
def write5(stream: BitStream, value: Int5): BitStream = stream.writeBits(toBits(value))
|
||||
|
||||
/**
|
||||
* read a 5bits value from a stream
|
||||
*
|
||||
* @param stream stream to read from
|
||||
* @return a (stream, value) pair
|
||||
*/
|
||||
def read5(stream: BitStream): (BitStream, Int5) = {
|
||||
val (stream1, bits) = stream.readBits(5)
|
||||
val value = (if (bits(0)) 1 << 4 else 0) + (if (bits(1)) 1 << 3 else 0) + (if (bits(2)) 1 << 2 else 0) + (if (bits(3)) 1 << 1 else 0) + (if (bits(4)) 1 << 0 else 0)
|
||||
(stream1, (value & 0xff).toByte)
|
||||
}
|
||||
|
||||
/**
|
||||
* splits a bit stream into 5bits values
|
||||
*
|
||||
* @param stream
|
||||
* @param acc
|
||||
* @return a sequence of 5bits values
|
||||
*/
|
||||
@tailrec
|
||||
def toInt5s(stream: BitStream, acc: Seq[Int5] = Nil): Seq[Int5] = if (stream.bitCount == 0) acc else {
|
||||
val (stream1, value) = read5(stream)
|
||||
toInt5s(stream1, acc :+ value)
|
||||
}
|
||||
|
||||
/**
|
||||
* prepend an unsigned long value to a sequence of Int5s
|
||||
*
|
||||
* @param value input value
|
||||
* @param acc sequence of Int5 values
|
||||
* @return an update sequence of Int5s
|
||||
*/
|
||||
@tailrec
|
||||
def writeUnsignedLong(value: Long, acc: Seq[Int5] = Nil): Seq[Int5] = {
|
||||
require(value >= 0)
|
||||
if (value == 0) acc
|
||||
else writeUnsignedLong(value / 32, (value % 32).toByte +: acc)
|
||||
}
|
||||
|
||||
/**
|
||||
* convert a tag data size to a sequence of Int5s. It * must * fit on a sequence
|
||||
* of 2 Int5 values
|
||||
*
|
||||
* @param size data size
|
||||
* @return size as a sequence of exactly 2 Int5 values
|
||||
*/
|
||||
def writeSize(size: Long): Seq[Int5] = {
|
||||
val output = writeUnsignedLong(size)
|
||||
// make sure that size is encoded on 2 int5 values
|
||||
output.length match {
|
||||
case 0 => Seq(0.toByte, 0.toByte)
|
||||
case 1 => 0.toByte +: output
|
||||
case 2 => output
|
||||
case n => throw new IllegalArgumentException("tag data length field must be encoded on 2 5-bits integers")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads an unsigned long value from a sequence of Int5s
|
||||
*
|
||||
* @param length length of the sequence
|
||||
* @param ints sequence of Int5s
|
||||
* @return an unsigned long value
|
||||
*/
|
||||
def readUnsignedLong(length: Int, ints: Seq[Int5]): Long = ints.take(length).foldLeft(0L) { case (acc, i) => acc * 32 + i }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input bech32-encoded payment request
|
||||
* @return a payment request
|
||||
*/
|
||||
def read(input: String): PaymentRequest = {
|
||||
val (hrp, data) = Bech32.decode(input)
|
||||
val stream = data.foldLeft(BitStream.empty)(write5)
|
||||
require(stream.bitCount >= 65 * 8, "data is too short to contain a 65 bytes signature")
|
||||
val (stream1, sig) = stream.popBytes(65)
|
||||
|
||||
val data0 = toInt5s(stream1)
|
||||
val timestamp = Timestamp.decode(data0)
|
||||
val data1 = data0.drop(7)
|
||||
|
||||
@tailrec
|
||||
def loop(data: Seq[Int5], tags: Seq[Seq[Int5]] = Nil): Seq[Seq[Int5]] = if (data.isEmpty) tags else {
|
||||
// 104 is the size of a signature
|
||||
val len = 1 + 2 + 32 * data(1) + data(2)
|
||||
loop(data.drop(len), tags :+ data.take(len))
|
||||
}
|
||||
|
||||
val rawtags = loop(data1)
|
||||
val tags = rawtags.map(Tag.parse)
|
||||
val signature = sig.reverse
|
||||
val r = new BigInteger(1, signature.take(32).toArray)
|
||||
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
|
||||
val recid = signature.last
|
||||
val message: BinaryData = hrp.getBytes ++ stream1.bytes
|
||||
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), Crypto.sha256(message))
|
||||
val pub = if (recid % 2 != 0) pub2 else pub1
|
||||
val prefix = hrp.take(4)
|
||||
val amount_opt = Amount.decode(hrp.drop(4))
|
||||
val pr = PaymentRequest(prefix, amount_opt, timestamp, pub, tags.toList, signature)
|
||||
val validSig = Crypto.verifySignature(Crypto.sha256(message), (r, s), pub)
|
||||
require(validSig, "invalid signature")
|
||||
pr
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pr payment request
|
||||
* @return a bech32-encoded payment request
|
||||
*/
|
||||
def write(pr: PaymentRequest): String = {
|
||||
// currency unit is Satoshi, but we compute amounts in Millisatoshis
|
||||
val hramount = Amount.encode(pr.amount)
|
||||
val hrp = s"${pr.prefix}$hramount"
|
||||
val stream = pr.stream.writeBytes(pr.signature)
|
||||
val checksum = Bech32.checksum(hrp, toInt5s(stream))
|
||||
hrp + "1" + new String((toInt5s(stream) ++ checksum).map(i => Bech32.pam(i)).toArray)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, NodeParams}
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, DecodeResult}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
sealed trait Origin
|
||||
case class Local(sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors
|
||||
case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin
|
||||
|
||||
case class ForwardAdd(add: UpdateAddHtlc)
|
||||
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin)
|
||||
case class ForwardLocalFail(error: Throwable, to: Origin) // happens when the failure happened in a local channel (and not in some downstream channel)
|
||||
case class ForwardFail(fail: UpdateFailHtlc, to: Origin)
|
||||
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin)
|
||||
|
||||
case class AckFulfillCmd(channelId: BinaryData, htlcId: Long)
|
||||
|
||||
// @formatter:on
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 01/02/2017.
|
||||
*/
|
||||
class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) extends Actor with ActorLogging {
|
||||
|
||||
import nodeParams.preimagesDb
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
|
||||
|
||||
override def receive: Receive = main(Map())
|
||||
|
||||
def main(channelUpdates: Map[Long, ChannelUpdate]): Receive = {
|
||||
|
||||
case ChannelStateChanged(channel, _, _, _, NORMAL | SHUTDOWN | CLOSING, d: HasCommitments) =>
|
||||
import d.channelId
|
||||
preimagesDb.listPreimages(channelId) match {
|
||||
case Nil => {}
|
||||
case preimages =>
|
||||
log.info(s"re-sending ${preimages.size} unacked fulfills to channel $channelId")
|
||||
preimages.map(p => CMD_FULFILL_HTLC(p._2, p._3, commit = false)).foreach(channel ! _)
|
||||
// better to sign once instead of after each fulfill
|
||||
channel ! CMD_SIGN
|
||||
}
|
||||
|
||||
case channelUpdate: ChannelUpdate =>
|
||||
log.info(s"updating relay parameters with channelUpdate=$channelUpdate")
|
||||
context become main(channelUpdates + (channelUpdate.shortChannelId -> channelUpdate))
|
||||
|
||||
case ForwardAdd(add) =>
|
||||
Try(Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket))
|
||||
.map {
|
||||
case Sphinx.ParsedPacket(payload, nextPacket, sharedSecret) => (LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload.data)), nextPacket, sharedSecret)
|
||||
} match {
|
||||
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) if nextPacket.isLastPacket =>
|
||||
log.info(s"looks like we are the final recipient of htlc #${add.id}")
|
||||
perHopPayload match {
|
||||
case PerHopPayload(_, finalAmountToForward, _) if finalAmountToForward > add.amountMsat =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(FinalIncorrectHtlcAmount(add.amountMsat)), commit = true)
|
||||
case PerHopPayload(_, _, finalOutgoingCltvValue) if finalOutgoingCltvValue != add.expiry =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(FinalIncorrectCltvExpiry(add.expiry)), commit = true)
|
||||
case _ if add.expiry < Globals.blockCount.get() + 3 => // TODO: check hardcoded value
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(FinalExpiryTooSoon), commit = true)
|
||||
case _ =>
|
||||
paymentHandler forward add
|
||||
}
|
||||
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) =>
|
||||
val channelUpdate_opt = channelUpdates.get(perHopPayload.channel_id)
|
||||
channelUpdate_opt match {
|
||||
case None =>
|
||||
// TODO: clarify what we're supposed to do in the specs
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(TemporaryNodeFailure), commit = true)
|
||||
case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.flags) =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.flags, channelUpdate)), commit = true)
|
||||
case Some(channelUpdate) if add.amountMsat < channelUpdate.htlcMinimumMsat =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(add.amountMsat, channelUpdate)), commit = true)
|
||||
case Some(channelUpdate) if add.expiry != perHopPayload.outgoingCltvValue + channelUpdate.cltvExpiryDelta =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(add.expiry, channelUpdate)), commit = true)
|
||||
case Some(channelUpdate) if add.expiry < Globals.blockCount.get() + 3 => // TODO: hardcoded value
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
|
||||
case _ =>
|
||||
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}")
|
||||
register forward Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
|
||||
}
|
||||
case Success((Attempt.Failure(cause), _, _)) =>
|
||||
log.error(s"couldn't parse payload: $cause")
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(PermanentNodeFailure), commit = true)
|
||||
case Failure(t) =>
|
||||
log.error(t, "couldn't parse onion: ")
|
||||
// we cannot even parse the onion packet
|
||||
sender ! CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true)
|
||||
}
|
||||
|
||||
case Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _))) =>
|
||||
log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}")
|
||||
register ! Register.Forward(add.channelId, CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true))
|
||||
|
||||
case ForwardFulfill(fulfill, Local(Some(sender))) =>
|
||||
sender ! fulfill
|
||||
|
||||
case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut)) =>
|
||||
val cmd = CMD_FULFILL_HTLC(originHtlcId, fulfill.paymentPreimage, commit = true)
|
||||
register ! Register.Forward(originChannelId, cmd)
|
||||
context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), Crypto.sha256(fulfill.paymentPreimage)))
|
||||
// we also store the preimage in a db (note that this happens *after* forwarding the fulfill to the channel, so we don't add latency)
|
||||
preimagesDb.addPreimage(originChannelId, originHtlcId, fulfill.paymentPreimage)
|
||||
|
||||
case AckFulfillCmd(channelId, htlcId) =>
|
||||
log.debug(s"fulfill acked for channelId=$channelId htlcId=$htlcId")
|
||||
preimagesDb.removePreimage(channelId, htlcId)
|
||||
|
||||
case ForwardLocalFail(error, Local(Some(sender))) =>
|
||||
sender ! Status.Failure(error)
|
||||
|
||||
case ForwardLocalFail(error, Relayed(originChannelId, originHtlcId, _, _)) =>
|
||||
// TODO: clarify what we're supposed to do in the specs depending on the error
|
||||
val failure = error match {
|
||||
case HtlcTimedout(_) => PermanentChannelFailure
|
||||
case _ => TemporaryNodeFailure
|
||||
}
|
||||
val cmd = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
|
||||
register ! Register.Forward(originChannelId, cmd)
|
||||
|
||||
case ForwardFail(fail, Local(Some(sender))) =>
|
||||
sender ! fail
|
||||
|
||||
case ForwardFail(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
|
||||
val cmd = CMD_FAIL_HTLC(originHtlcId, Left(fail.reason), commit = true)
|
||||
register ! Register.Forward(originChannelId, cmd)
|
||||
|
||||
case ForwardFailMalformed(fail, Local(Some(sender))) =>
|
||||
sender ! fail
|
||||
|
||||
case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _)) =>
|
||||
val cmd = CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
|
||||
register ! Register.Forward(originChannelId, cmd)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Relayer {
|
||||
def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler)
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering}
|
||||
import fr.acinq.eclair.serializationResult
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, LightningMessageCodecs, NodeAnnouncement}
|
||||
import scodec.bits.BitVector
|
||||
import shapeless.HNil
|
||||
|
||||
import scala.compat.Platform
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 03/02/2017.
|
||||
*/
|
||||
object Announcements {
|
||||
|
||||
def channelAnnouncementWitnessEncode(chainHash: BinaryData, shortChannelId: Long, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: BinaryData): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: HNil))))
|
||||
|
||||
def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: (Byte, Byte, Byte), alias: String, features: BinaryData, addresses: List[InetSocketAddress]): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: HNil))))
|
||||
|
||||
def channelUpdateWitnessEncode(chainHash: BinaryData, shortChannelId: Long, timestamp: Long, flags: BinaryData, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long): BinaryData =
|
||||
sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: flags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: HNil))))
|
||||
|
||||
def signChannelAnnouncement(chainHash: BinaryData, shortChannelId: Long, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: BinaryData): (BinaryData, BinaryData) = {
|
||||
val witness = if (isNode1(localNodeSecret.publicKey.toBin, remoteNodeId.toBin)) {
|
||||
channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeSecret.publicKey, remoteNodeId, localFundingPrivKey.publicKey, remoteFundingKey, features)
|
||||
} else {
|
||||
channelAnnouncementWitnessEncode(chainHash, shortChannelId, remoteNodeId, localNodeSecret.publicKey, remoteFundingKey, localFundingPrivKey.publicKey, features)
|
||||
}
|
||||
val nodeSig = Crypto.encodeSignature(Crypto.sign(witness, localNodeSecret)) :+ 1.toByte
|
||||
val bitcoinSig = Crypto.encodeSignature(Crypto.sign(witness, localFundingPrivKey)) :+ 1.toByte
|
||||
(nodeSig, bitcoinSig)
|
||||
}
|
||||
|
||||
def makeChannelAnnouncement(chainHash: BinaryData, shortChannelId: Long, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: BinaryData, remoteNodeSignature: BinaryData, localBitcoinSignature: BinaryData, remoteBitcoinSignature: BinaryData): ChannelAnnouncement = {
|
||||
val (nodeId1, nodeId2, bitcoinKey1, bitcoinKey2, nodeSignature1, nodeSignature2, bitcoinSignature1, bitcoinSignature2) =
|
||||
if (isNode1(localNodeId.toBin, remoteNodeId.toBin)) {
|
||||
(localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, localNodeSignature, remoteNodeSignature, localBitcoinSignature, remoteBitcoinSignature)
|
||||
} else {
|
||||
(remoteNodeId, localNodeId, remoteFundingKey, localFundingKey, remoteNodeSignature, localNodeSignature, remoteBitcoinSignature, localBitcoinSignature)
|
||||
}
|
||||
ChannelAnnouncement(
|
||||
nodeSignature1 = nodeSignature1,
|
||||
nodeSignature2 = nodeSignature2,
|
||||
bitcoinSignature1 = bitcoinSignature1,
|
||||
bitcoinSignature2 = bitcoinSignature2,
|
||||
shortChannelId = shortChannelId,
|
||||
nodeId1 = nodeId1,
|
||||
nodeId2 = nodeId2,
|
||||
bitcoinKey1 = bitcoinKey1,
|
||||
bitcoinKey2 = bitcoinKey2,
|
||||
features = BinaryData(""),
|
||||
chainHash = chainHash
|
||||
)
|
||||
}
|
||||
|
||||
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: (Byte, Byte, Byte), addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = {
|
||||
require(alias.size <= 32)
|
||||
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", addresses)
|
||||
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
|
||||
NodeAnnouncement(
|
||||
signature = sig,
|
||||
timestamp = timestamp,
|
||||
nodeId = nodeSecret.publicKey,
|
||||
rgbColor = color,
|
||||
alias = alias,
|
||||
features = "",
|
||||
addresses = addresses
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* BOLT 7:
|
||||
* The creating node MUST set node-id-1 and node-id-2 to the public keys of the
|
||||
* two nodes who are operating the channel, such that node-id-1 is the numerically-lesser
|
||||
* of the two DER encoded keys sorted in ascending numerical order,
|
||||
*
|
||||
* @return true if localNodeId is node1
|
||||
*/
|
||||
def isNode1(localNodeId: BinaryData, remoteNodeId: BinaryData) = LexicographicalOrdering.isLessThan(localNodeId, remoteNodeId)
|
||||
|
||||
/**
|
||||
* BOLT 7:
|
||||
* The creating node [...] MUST set the direction bit of flags to 0 if
|
||||
* the creating node is node-id-1 in that message, otherwise 1.
|
||||
*
|
||||
* @return true if the node who sent these flags is node1
|
||||
*/
|
||||
def isNode1(flags: BinaryData) = !BitVector(flags.data).reverse.get(0)
|
||||
|
||||
/**
|
||||
* A node MAY create and send a channel_update with the disable bit set to
|
||||
* signal the temporary unavailability of a channel
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def isEnabled(flags: BinaryData) = !BitVector(flags.data).reverse.get(1)
|
||||
|
||||
def makeFlags(isNode1: Boolean, enable: Boolean): BinaryData = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(16).toByteArray
|
||||
|
||||
def makeChannelUpdate(chainHash: BinaryData, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: Long, cltvExpiryDelta: Int, htlcMinimumMsat: Long, feeBaseMsat: Long, feeProportionalMillionths: Long, enable: Boolean = true, timestamp: Long = Platform.currentTime / 1000): ChannelUpdate = {
|
||||
val flags = makeFlags(isNode1 = isNode1(nodeSecret.publicKey.toBin, remoteNodeId.toBin), enable = enable)
|
||||
require(flags.size == 2, "flags must be a 2-bytes field")
|
||||
val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, flags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths)
|
||||
val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte
|
||||
ChannelUpdate(
|
||||
signature = sig,
|
||||
chainHash = chainHash,
|
||||
shortChannelId = shortChannelId,
|
||||
timestamp = timestamp,
|
||||
flags = flags,
|
||||
cltvExpiryDelta = cltvExpiryDelta,
|
||||
htlcMinimumMsat = htlcMinimumMsat,
|
||||
feeBaseMsat = feeBaseMsat,
|
||||
feeProportionalMillionths = feeProportionalMillionths
|
||||
)
|
||||
}
|
||||
|
||||
def checkSigs(ann: ChannelAnnouncement): Boolean = {
|
||||
val witness = channelAnnouncementWitnessEncode(ann.chainHash, ann.shortChannelId, ann.nodeId1, ann.nodeId2, ann.bitcoinKey1, ann.bitcoinKey2, ann.features)
|
||||
verifySignature(witness, ann.nodeSignature1, ann.nodeId1) &&
|
||||
verifySignature(witness, ann.nodeSignature2, ann.nodeId2) &&
|
||||
verifySignature(witness, ann.bitcoinSignature1, ann.bitcoinKey1) &&
|
||||
verifySignature(witness, ann.bitcoinSignature2, ann.bitcoinKey2)
|
||||
}
|
||||
|
||||
def checkSig(ann: NodeAnnouncement): Boolean = {
|
||||
val witness = nodeAnnouncementWitnessEncode(ann.timestamp, ann.nodeId, ann.rgbColor, ann.alias, ann.features, ann.addresses)
|
||||
verifySignature(witness, ann.signature, ann.nodeId)
|
||||
}
|
||||
|
||||
def checkSig(ann: ChannelUpdate, nodeId: PublicKey): Boolean = {
|
||||
val witness = channelUpdateWitnessEncode(ann.chainHash, ann.shortChannelId, ann.timestamp, ann.flags, ann.cltvExpiryDelta, ann.htlcMinimumMsat, ann.feeBaseMsat, ann.feeProportionalMillionths)
|
||||
verifySignature(witness, ann.signature, nodeId)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,443 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import java.io.StringWriter
|
||||
|
||||
import akka.actor.{ActorRef, FSM, Props}
|
||||
import akka.pattern.pipe
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Script.{pay2wsh, write}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.io.Peer
|
||||
import fr.acinq.eclair.payment.Hop
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
|
||||
import org.jgrapht.ext._
|
||||
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge, SimpleGraph}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Random, Success, Try}
|
||||
|
||||
// @formatter:off
|
||||
|
||||
case class ChannelDesc(id: Long, a: PublicKey, b: PublicKey)
|
||||
case class RouteRequest(source: PublicKey, target: PublicKey, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[Long] = Set.empty)
|
||||
case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long]) { require(hops.size > 0, "route cannot be empty") }
|
||||
case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed)
|
||||
case class LiftChannelExclusion(desc: ChannelDesc)
|
||||
case class SendRoutingState(to: ActorRef)
|
||||
case class Rebroadcast(ann: Seq[RoutingMessage], origins: Map[RoutingMessage, ActorRef])
|
||||
|
||||
case class Data(nodes: Map[PublicKey, NodeAnnouncement],
|
||||
channels: Map[Long, ChannelAnnouncement],
|
||||
updates: Map[ChannelDesc, ChannelUpdate],
|
||||
rebroadcast: Seq[RoutingMessage],
|
||||
stash: Seq[RoutingMessage],
|
||||
awaiting: Seq[ChannelAnnouncement],
|
||||
origins: Map[RoutingMessage, ActorRef],
|
||||
localChannels: Map[BinaryData, PublicKey],
|
||||
excludedChannels: Set[ChannelDesc]) // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure
|
||||
|
||||
sealed trait State
|
||||
case object NORMAL extends State
|
||||
case object WAITING_FOR_VALIDATION extends State
|
||||
|
||||
case object TickBroadcast
|
||||
case object TickValidate
|
||||
case object TickPruneStaleChannels
|
||||
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Created by PM on 24/05/2016.
|
||||
*/
|
||||
|
||||
class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data] {
|
||||
|
||||
import Router._
|
||||
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
|
||||
|
||||
setTimer(TickBroadcast.toString, TickBroadcast, nodeParams.routerBroadcastInterval, repeat = true)
|
||||
setTimer(TickValidate.toString, TickValidate, nodeParams.routerValidateInterval, repeat = true)
|
||||
setTimer(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 day, repeat = true)
|
||||
|
||||
val db = nodeParams.networkDb
|
||||
|
||||
// Note: We go through the whole validation process instead of directly loading into memory, because the channels
|
||||
// could have been closed while we were shutdown, and if someone connects to us right after startup we don't want to
|
||||
// advertise invalid channels. We could optimize this (at least not fetch txes from the blockchain, and not check sigs)
|
||||
log.info(s"loading network announcements from db...")
|
||||
db.listChannels().map(self ! _)
|
||||
db.listNodes().map(self ! _)
|
||||
db.listChannelUpdates().map(self ! _)
|
||||
if (db.listChannels().size > 0) {
|
||||
val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, Platform.currentTime / 1000)
|
||||
self ! nodeAnn
|
||||
}
|
||||
log.info(s"starting state machine")
|
||||
|
||||
startWith(NORMAL, Data(Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Map.empty, Map.empty, Set.empty))
|
||||
|
||||
when(NORMAL) {
|
||||
case Event(TickValidate, d) =>
|
||||
require(d.awaiting.size == 0)
|
||||
var i = 0
|
||||
// we extract a batch of channel announcements from the stash
|
||||
val (channelAnns: Seq[ChannelAnnouncement]@unchecked, otherAnns) = d.stash.partition {
|
||||
case _: ChannelAnnouncement =>
|
||||
i = i + 1
|
||||
i <= MAX_PARALLEL_JSONRPC_REQUESTS
|
||||
case _ => false
|
||||
}
|
||||
if (channelAnns.size > 0) {
|
||||
log.info(s"validating a batch of ${channelAnns.size} channels")
|
||||
watcher ! ParallelGetRequest(channelAnns)
|
||||
goto(WAITING_FOR_VALIDATION) using d.copy(stash = otherAnns, awaiting = channelAnns)
|
||||
} else stay
|
||||
}
|
||||
|
||||
when(WAITING_FOR_VALIDATION) {
|
||||
case Event(ParallelGetResponse(results), d) =>
|
||||
val validated = results.map {
|
||||
case IndividualResult(c, Some(tx), true) =>
|
||||
// TODO: blacklisting
|
||||
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
|
||||
// let's check that the output is indeed a P2WSH multisig 2-of-2 of nodeid1 and nodeid2)
|
||||
val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
|
||||
if (tx.txOut.size < outputIndex + 1) {
|
||||
log.error(s"invalid script for shortChannelId=${c.shortChannelId}: txid=${tx.txid} does not have outputIndex=$outputIndex ann=$c")
|
||||
None
|
||||
} else if (fundingOutputScript != tx.txOut(outputIndex).publicKeyScript) {
|
||||
log.error(s"invalid script for shortChannelId=${c.shortChannelId} txid=${tx.txid} ann=$c")
|
||||
None
|
||||
} else {
|
||||
watcher ! WatchSpentBasic(self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId))
|
||||
// TODO: check feature bit set
|
||||
log.debug(s"added channel channelId=${c.shortChannelId}")
|
||||
context.system.eventStream.publish(ChannelDiscovered(c, tx.txOut(outputIndex).amount))
|
||||
db.addChannel(c)
|
||||
Some(c)
|
||||
}
|
||||
case IndividualResult(c, Some(tx), false) =>
|
||||
// TODO: vulnerability if they flood us with spent funding tx?
|
||||
log.warning(s"ignoring shortChannelId=${c.shortChannelId} tx=${tx.txid} (funding tx not found in utxo)")
|
||||
// there may be a record if we have just restarted
|
||||
db.removeChannel(c.shortChannelId)
|
||||
None
|
||||
case IndividualResult(c, None, _) =>
|
||||
// TODO: blacklist?
|
||||
log.warning(s"could not retrieve tx for shortChannelId=${c.shortChannelId}")
|
||||
None
|
||||
}.flatten
|
||||
// we reprocess node and channel-update announcements that may have been validated
|
||||
val (resend, stash1) = d.stash.partition {
|
||||
case n: NodeAnnouncement => results.exists(r => isRelatedTo(r.c, n.nodeId))
|
||||
case u: ChannelUpdate => results.exists(r => r.c.shortChannelId == u.shortChannelId)
|
||||
case _ => false
|
||||
}
|
||||
resend.foreach(self ! _)
|
||||
goto(NORMAL) using d.copy(channels = d.channels ++ validated.map(c => (c.shortChannelId -> c)), rebroadcast = d.rebroadcast ++ validated, stash = stash1, awaiting = Nil)
|
||||
}
|
||||
|
||||
whenUnhandled {
|
||||
case Event(ChannelStateChanged(_, _, _, _, channel.NORMAL, d: DATA_NORMAL), d1) =>
|
||||
stay using d1.copy(localChannels = d1.localChannels + (d.commitments.channelId -> d.commitments.remoteParams.nodeId))
|
||||
|
||||
case Event(ChannelStateChanged(_, _, _, channel.NORMAL, _, d: DATA_NEGOTIATING), d1) =>
|
||||
stay using d1.copy(localChannels = d1.localChannels - d.commitments.channelId)
|
||||
|
||||
case Event(_: ChannelStateChanged, _) => stay
|
||||
|
||||
case Event(SendRoutingState(remote), Data(nodes, channels, updates, _, _, _, _, _, _)) =>
|
||||
log.debug(s"info sending all announcements to $remote: channels=${channels.size} nodes=${nodes.size} updates=${updates.size}")
|
||||
// we group and add delays to leave room for channel messages
|
||||
context.actorOf(ThrottleForwarder.props(remote, channels.values ++ nodes.values ++ updates.values, 100, 100 millis))
|
||||
stay
|
||||
|
||||
case Event(c: ChannelAnnouncement, d) =>
|
||||
log.debug(s"received channel announcement for shortChannelId=${c.shortChannelId} nodeId1=${c.nodeId1} nodeId2=${c.nodeId2}")
|
||||
if (d.channels.containsKey(c.shortChannelId) || d.awaiting.exists(_.shortChannelId == c.shortChannelId) || d.stash.contains(c)) {
|
||||
log.debug(s"ignoring $c (duplicate)")
|
||||
stay
|
||||
} else if (!Announcements.checkSigs(c)) {
|
||||
log.error(s"bad signature for announcement $c")
|
||||
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
|
||||
stay
|
||||
} else {
|
||||
log.debug(s"stashing $c")
|
||||
stay using d.copy(stash = d.stash :+ c, origins = d.origins + (c -> sender))
|
||||
}
|
||||
|
||||
case Event(n: NodeAnnouncement, d: Data) =>
|
||||
if (d.nodes.containsKey(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) {
|
||||
log.debug(s"ignoring announcement $n (old timestamp or duplicate)")
|
||||
stay
|
||||
} else if (!Announcements.checkSig(n)) {
|
||||
log.error(s"bad signature for announcement $n")
|
||||
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
|
||||
stay
|
||||
} else if (d.nodes.containsKey(n.nodeId)) {
|
||||
log.debug(s"updated node nodeId=${n.nodeId}")
|
||||
context.system.eventStream.publish(NodeUpdated(n))
|
||||
db.updateNode(n)
|
||||
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
|
||||
} else if (d.channels.values.exists(c => isRelatedTo(c, n.nodeId))) {
|
||||
log.debug(s"added node nodeId=${n.nodeId}")
|
||||
context.system.eventStream.publish(NodeDiscovered(n))
|
||||
db.addNode(n)
|
||||
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
|
||||
} else if (d.awaiting.exists(c => isRelatedTo(c, n.nodeId)) || d.stash.collectFirst { case c: ChannelAnnouncement if isRelatedTo(c, n.nodeId) => c }.isDefined) {
|
||||
log.debug(s"stashing $n")
|
||||
stay using d.copy(stash = d.stash :+ n, origins = d.origins + (n -> sender))
|
||||
} else {
|
||||
log.warning(s"ignoring $n (no related channel found)")
|
||||
// there may be a record if we have just restarted
|
||||
db.removeNode(n.nodeId)
|
||||
stay
|
||||
}
|
||||
|
||||
case Event(u: ChannelUpdate, d: Data) =>
|
||||
if (d.channels.contains(u.shortChannelId)) {
|
||||
val c = d.channels(u.shortChannelId)
|
||||
val desc = getDesc(u, c)
|
||||
if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) {
|
||||
log.debug(s"ignoring $u (old timestamp or duplicate)")
|
||||
stay
|
||||
} else if (!Announcements.checkSig(u, getDesc(u, d.channels(u.shortChannelId)).a)) {
|
||||
// TODO: (dirty) this will make the origin channel close the connection
|
||||
log.error(s"bad signature for announcement $u")
|
||||
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
|
||||
stay
|
||||
} else if (d.updates.contains(desc)) {
|
||||
log.debug(s"updated $u")
|
||||
context.system.eventStream.publish(ChannelUpdateReceived(u))
|
||||
db.updateChannelUpdate(u)
|
||||
stay using d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast :+ u, origins = d.origins + (u -> sender))
|
||||
} else {
|
||||
log.debug(s"added $u")
|
||||
context.system.eventStream.publish(ChannelUpdateReceived(u))
|
||||
db.addChannelUpdate(u)
|
||||
stay using d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast :+ u, origins = d.origins + (u -> sender))
|
||||
}
|
||||
} else if (d.awaiting.exists(c => c.shortChannelId == u.shortChannelId) || d.stash.collectFirst { case c: ChannelAnnouncement if c.shortChannelId == u.shortChannelId => c }.isDefined) {
|
||||
log.debug(s"stashing $u")
|
||||
stay using d.copy(stash = d.stash :+ u, origins = d.origins + (u -> sender))
|
||||
} else {
|
||||
log.warning(s"ignoring announcement $u (unknown channel)")
|
||||
stay
|
||||
}
|
||||
|
||||
case Event(WatchEventSpentBasic(BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(shortChannelId)), d)
|
||||
if d.channels.containsKey(shortChannelId) =>
|
||||
val lostChannel = d.channels(shortChannelId)
|
||||
log.info(s"funding tx of channelId=$shortChannelId has been spent")
|
||||
// we need to remove nodes that aren't tied to any channels anymore
|
||||
val channels1 = d.channels - lostChannel.shortChannelId
|
||||
val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values))
|
||||
// let's clean the db and send the events
|
||||
log.info(s"pruning shortChannelId=$shortChannelId (spent)")
|
||||
db.removeChannel(shortChannelId) // NB: this also removes channel updates
|
||||
context.system.eventStream.publish(ChannelLost(shortChannelId))
|
||||
lostNodes.foreach {
|
||||
case nodeId =>
|
||||
log.info(s"pruning nodeId=$nodeId (spent)")
|
||||
db.removeNode(nodeId)
|
||||
context.system.eventStream.publish(NodeLost(nodeId))
|
||||
}
|
||||
stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, updates = d.updates.filterKeys(_.id != shortChannelId))
|
||||
|
||||
case Event(TickValidate, d) => stay // ignored
|
||||
|
||||
case Event(TickBroadcast, d) =>
|
||||
d.rebroadcast match {
|
||||
case Nil => stay using d.copy(origins = Map.empty)
|
||||
case _ =>
|
||||
log.info(s"broadcasting ${d.rebroadcast.size} routing messages")
|
||||
context.actorSelection(context.system / "*" / "switchboard") ! Rebroadcast(d.rebroadcast, d.origins)
|
||||
stay using d.copy(rebroadcast = Nil, origins = Map.empty)
|
||||
}
|
||||
|
||||
case Event(TickPruneStaleChannels, d) =>
|
||||
// first we select channels that we will prune
|
||||
val staleChannels = getStaleChannels(d.channels, d.updates)
|
||||
// then we clean up the related channel updates
|
||||
val staleUpdates = d.updates.keys.filter(desc => staleChannels.contains(desc.id))
|
||||
// finally we remove nodes that aren't tied to any channels anymore
|
||||
val channels1 = d.channels -- staleChannels
|
||||
val staleNodes = d.nodes.keys.filterNot(nodeId => hasChannels(nodeId, channels1.values))
|
||||
// let's clean the db and send the events
|
||||
staleChannels.foreach {
|
||||
case shortChannelId =>
|
||||
log.info(s"pruning shortChannelId=$shortChannelId (stale)")
|
||||
db.removeChannel(shortChannelId) // NB: this also removes channel updates
|
||||
context.system.eventStream.publish(ChannelLost(shortChannelId))
|
||||
}
|
||||
staleNodes.foreach {
|
||||
case nodeId =>
|
||||
log.info(s"pruning nodeId=$nodeId (stale)")
|
||||
db.removeNode(nodeId)
|
||||
context.system.eventStream.publish(NodeLost(nodeId))
|
||||
}
|
||||
stay using d.copy(nodes = d.nodes -- staleNodes, channels = channels1, updates = d.updates -- staleUpdates)
|
||||
|
||||
case Event(ExcludeChannel(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
|
||||
val banDuration = nodeParams.channelExcludeDuration
|
||||
log.info(s"excluding shortChannelId=$shortChannelId from nodeId=$nodeId for duration=$banDuration")
|
||||
context.system.scheduler.scheduleOnce(banDuration, self, LiftChannelExclusion(desc))
|
||||
stay using d.copy(excludedChannels = d.excludedChannels + desc)
|
||||
|
||||
case Event(LiftChannelExclusion(desc@ChannelDesc(shortChannelId, nodeId, _)), d) =>
|
||||
log.info(s"reinstating shortChannelId=$shortChannelId from nodeId=$nodeId")
|
||||
stay using d.copy(excludedChannels = d.excludedChannels - desc)
|
||||
|
||||
case Event('nodes, d) =>
|
||||
sender ! d.nodes.values
|
||||
stay
|
||||
|
||||
case Event('channels, d) =>
|
||||
sender ! d.channels.values
|
||||
stay
|
||||
|
||||
case Event('updates, d) =>
|
||||
sender ! d.updates.values
|
||||
stay
|
||||
|
||||
case Event('dot, d) =>
|
||||
graph2dot(d.nodes, d.channels) pipeTo sender
|
||||
stay
|
||||
|
||||
case Event(RouteRequest(start, end, ignoreNodes, ignoreChannels), d) =>
|
||||
val localNodeId = nodeParams.privateKey.publicKey
|
||||
// TODO: HACK!!!!! the following is a workaround to make our routing work with private/not-yet-announced channels, that do not have a channelUpdate
|
||||
val fakeUpdates = d.localChannels.map { case (channelId, remoteNodeId) =>
|
||||
// note that this id is deterministic, otherwise filterUpdates would not work
|
||||
val fakeShortId = BigInt(channelId.take(7).toArray).toLong
|
||||
val channelDesc = ChannelDesc(fakeShortId, localNodeId, remoteNodeId)
|
||||
// note that we store the channelId in the sig, other values are not used because if it is selected this will be the first channel in the route
|
||||
val channelUpdate = ChannelUpdate(signature = channelId, chainHash = nodeParams.chainHash, fakeShortId, 0, "0000", 0, 0, 0, 0)
|
||||
(channelDesc -> channelUpdate)
|
||||
}
|
||||
// we replace local channelUpdates (we have them for regular public already-announced channels) by the ones we just generated
|
||||
val updates1 = d.updates.filterKeys(_.a != localNodeId) ++ fakeUpdates
|
||||
// we then filter out the currently excluded channels
|
||||
val updates2 = updates1.filterKeys(!d.excludedChannels.contains(_))
|
||||
// we also filter out excluded channels
|
||||
val updates3 = filterUpdates(updates2, ignoreNodes, ignoreChannels)
|
||||
log.info(s"finding a route $start->$end with ignoreNodes=${ignoreNodes.map(_.toBin).mkString(",")} ignoreChannels=${ignoreChannels.map(_.toHexString).mkString(",")}")
|
||||
findRoute(start, end, updates3).map(r => RouteResponse(r, ignoreNodes, ignoreChannels)) pipeTo sender
|
||||
stay
|
||||
}
|
||||
|
||||
onTransition {
|
||||
case _ -> NORMAL => log.info(s"current status channels=${nextStateData.channels.size} nodes=${nextStateData.nodes.size} updates=${nextStateData.updates.size}")
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
||||
}
|
||||
|
||||
object Router {
|
||||
|
||||
val MAX_PARALLEL_JSONRPC_REQUESTS = 50
|
||||
|
||||
def props(nodeParams: NodeParams, watcher: ActorRef) = Props(new Router(nodeParams, watcher))
|
||||
|
||||
def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = {
|
||||
require(u.flags.data.size == 2, s"invalid flags length ${u.flags.data.size} != 2")
|
||||
// the least significant bit tells us if it is node1 or node2
|
||||
if (Announcements.isNode1(u.flags)) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1)
|
||||
}
|
||||
|
||||
def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2
|
||||
|
||||
def hasChannels(nodeId: PublicKey, channels: Iterable[ChannelAnnouncement]): Boolean = channels.exists(c => isRelatedTo(c, nodeId))
|
||||
|
||||
def getStaleChannels(channels: Map[Long, ChannelAnnouncement], updates: Map[ChannelDesc, ChannelUpdate]): Iterable[Long] = {
|
||||
// BOLT 7: "nodes MAY prune channels should the timestamp of the latest channel_update be older than 2 weeks (1209600 seconds)"
|
||||
// but we don't want to prune brand new channels for which we didn't yet receive a channel update
|
||||
// so we consider stale a channel that:
|
||||
// (1) is older than 2 weeks (2*7*144 = 2016 blocks)
|
||||
// AND
|
||||
// (2) didn't have an update during the last 2 weeks
|
||||
val staleThresholdSeconds = Platform.currentTime / 1000 - 1209600
|
||||
val staleThresholdBlocks = Globals.blockCount.get() - 2016
|
||||
val staleChannels = channels
|
||||
.filterKeys(shortChannelId => fromShortId(shortChannelId)._1 < staleThresholdBlocks) // consider only channels older than 2 weeks
|
||||
.filterKeys(shortChannelId => !updates.values.exists(u => u.shortChannelId == shortChannelId && u.timestamp >= staleThresholdSeconds)) // no update in the past 2 weeks
|
||||
staleChannels.keys
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used after a payment failed, and we want to exclude some nodes/channels that we know are failing
|
||||
*/
|
||||
def filterUpdates(updates: Map[ChannelDesc, ChannelUpdate], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long]) =
|
||||
updates
|
||||
.filterNot(u => ignoreNodes.map(_.toBin).contains(u._1.a) || ignoreNodes.map(_.toBin).contains(u._1.b))
|
||||
.filterNot(u => ignoreChannels.contains(u._1.id))
|
||||
.filterNot(u => !Announcements.isEnabled(u._2.flags))
|
||||
|
||||
def findRouteDijkstra(localNodeId: PublicKey, targetNodeId: PublicKey, channels: Iterable[ChannelDesc]): Seq[ChannelDesc] = {
|
||||
if (localNodeId == targetNodeId) throw CannotRouteToSelf
|
||||
case class DescEdge(desc: ChannelDesc) extends DefaultEdge
|
||||
val g = new DefaultDirectedGraph[PublicKey, DescEdge](classOf[DescEdge])
|
||||
Random.shuffle(channels).foreach(d => {
|
||||
g.addVertex(d.a)
|
||||
g.addVertex(d.b)
|
||||
g.addEdge(d.a, d.b, new DescEdge(d))
|
||||
})
|
||||
Try(Option(DijkstraShortestPath.findPathBetween(g, localNodeId, targetNodeId))) match {
|
||||
case Success(Some(path)) => path.getEdgeList.map(_.desc)
|
||||
case _ => throw RouteNotFound
|
||||
}
|
||||
}
|
||||
|
||||
def findRoute(localNodeId: PublicKey, targetNodeId: PublicKey, updates: Map[ChannelDesc, ChannelUpdate])(implicit ec: ExecutionContext): Future[Seq[Hop]] = Future {
|
||||
findRouteDijkstra(localNodeId, targetNodeId, updates.keys)
|
||||
.map(desc => Hop(desc.a, desc.b, updates(desc)))
|
||||
}
|
||||
|
||||
def graph2dot(nodes: Map[PublicKey, NodeAnnouncement], channels: Map[Long, ChannelAnnouncement])(implicit ec: ExecutionContext): Future[String] = Future {
|
||||
case class DescEdge(channelId: Long) extends DefaultEdge
|
||||
val g = new SimpleGraph[PublicKey, DescEdge](classOf[DescEdge])
|
||||
channels.foreach(d => {
|
||||
g.addVertex(d._2.nodeId1)
|
||||
g.addVertex(d._2.nodeId2)
|
||||
g.addEdge(d._2.nodeId1, d._2.nodeId2, new DescEdge(d._1))
|
||||
})
|
||||
val vertexIDProvider = new ComponentNameProvider[PublicKey]() {
|
||||
override def getName(nodeId: PublicKey): String = "\"" + nodeId.toString() + "\""
|
||||
}
|
||||
val edgeLabelProvider = new ComponentNameProvider[DescEdge]() {
|
||||
override def getName(e: DescEdge): String = e.channelId.toString
|
||||
}
|
||||
val vertexAttributeProvider = new ComponentAttributeProvider[PublicKey]() {
|
||||
|
||||
override def getComponentAttributes(nodeId: PublicKey): java.util.Map[String, String] =
|
||||
|
||||
nodes.get(nodeId) match {
|
||||
case Some(ann) => Map("label" -> ann.alias, "color" -> f"#${ann.rgbColor._1}%02x${ann.rgbColor._2}%02x${ann.rgbColor._3}%02x")
|
||||
case None => Map.empty[String, String]
|
||||
}
|
||||
}
|
||||
val exporter = new DOTExporter[PublicKey, DescEdge](vertexIDProvider, null, edgeLabelProvider, vertexAttributeProvider, null)
|
||||
val writer = new StringWriter()
|
||||
try {
|
||||
exporter.exportGraph(g, writer)
|
||||
writer.toString
|
||||
} finally {
|
||||
writer.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
/**
|
||||
* Created by PM on 12/04/2017.
|
||||
*/
|
||||
|
||||
class RouterException(message: String) extends RuntimeException(message)
|
||||
|
||||
object RouteNotFound extends RouterException("Route not found")
|
||||
|
||||
object CannotRouteToSelf extends RouterException("Cannot route to self")
|
||||
@ -1,47 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
|
||||
import scala.concurrent.duration.{FiniteDuration, _}
|
||||
|
||||
/**
|
||||
* This actor forwards messages to another actor, but groups them and introduces
|
||||
* delays between each groups.
|
||||
*
|
||||
* If A wants to send a lot of lower importance messages to B, it is useful to let
|
||||
* higher importance messages go in the stream.
|
||||
*/
|
||||
class ThrottleForwarder(target: ActorRef, messages: Iterable[Any], chunkSize: Int, delay: FiniteDuration) extends Actor with ActorLogging {
|
||||
|
||||
import ThrottleForwarder.Tick
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
val clock = context.system.scheduler.schedule(0 second, delay, self, Tick)
|
||||
|
||||
log.debug(s"sending messages=${messages.size} with chunkSize=$chunkSize and delay=$delay")
|
||||
|
||||
override def receive = group(messages)
|
||||
|
||||
def group(messages: Iterable[Any]): Receive = {
|
||||
case Tick =>
|
||||
messages.splitAt(chunkSize) match {
|
||||
case (Nil, _) =>
|
||||
clock.cancel()
|
||||
log.debug(s"sent messages=${messages.size} with chunkSize=$chunkSize and delay=$delay")
|
||||
context stop self
|
||||
case (chunk, rest) =>
|
||||
chunk.foreach(target ! _)
|
||||
context become group(rest)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ThrottleForwarder {
|
||||
|
||||
def props(target: ActorRef, messages: Iterable[Any], groupSize: Int, delay: FiniteDuration) = Props(new ThrottleForwarder(target, messages, groupSize, delay))
|
||||
|
||||
case object Tick
|
||||
|
||||
}
|
||||
@ -1,235 +0,0 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction, TxOut}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.payment.{Local, Origin, Relayed}
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.LightningMessageCodecs._
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.codecs._
|
||||
import scodec.{Attempt, Codec}
|
||||
|
||||
/**
|
||||
* Created by PM on 02/06/2017.
|
||||
*/
|
||||
object ChannelCodecs {
|
||||
|
||||
val localParamsCodec: Codec[LocalParams] = (
|
||||
("nodeId" | publicKey) ::
|
||||
("dustLimitSatoshis" | uint64) ::
|
||||
("maxHtlcValueInFlightMsat" | uint64ex) ::
|
||||
("channelReserveSatoshis" | uint64) ::
|
||||
("htlcMinimumMsat" | uint64) ::
|
||||
("toSelfDelay" | uint16) ::
|
||||
("maxAcceptedHtlcs" | uint16) ::
|
||||
("fundingPrivKey" | privateKey) ::
|
||||
("revocationSecret" | scalar) ::
|
||||
("paymentKey" | scalar) ::
|
||||
("delayedPaymentKey" | scalar) ::
|
||||
("htlcKey" | scalar) ::
|
||||
("defaultFinalScriptPubKey" | varsizebinarydata) ::
|
||||
("shaSeed" | varsizebinarydata) ::
|
||||
("isFunder" | bool) ::
|
||||
("globalFeatures" | varsizebinarydata) ::
|
||||
("localFeatures" | varsizebinarydata)).as[LocalParams]
|
||||
|
||||
val remoteParamsCodec: Codec[RemoteParams] = (
|
||||
("nodeId" | publicKey) ::
|
||||
("dustLimitSatoshis" | uint64) ::
|
||||
("maxHtlcValueInFlightMsat" | uint64ex) ::
|
||||
("channelReserveSatoshis" | uint64) ::
|
||||
("htlcMinimumMsat" | uint64) ::
|
||||
("toSelfDelay" | uint16) ::
|
||||
("maxAcceptedHtlcs" | uint16) ::
|
||||
("fundingPubKey" | publicKey) ::
|
||||
("revocationBasepoint" | point) ::
|
||||
("paymentBasepoint" | point) ::
|
||||
("delayedPaymentBasepoint" | point) ::
|
||||
("htlcBasepoint" | point) ::
|
||||
("globalFeatures" | varsizebinarydata) ::
|
||||
("localFeatures" | varsizebinarydata)).as[RemoteParams]
|
||||
|
||||
val directionCodec: Codec[Direction] = Codec[Direction](
|
||||
(dir: Direction) => bool.encode(dir == IN),
|
||||
(wire: BitVector) => bool.decode(wire).map(_.map(b => if (b) IN else OUT))
|
||||
)
|
||||
|
||||
val htlcCodec: Codec[DirectedHtlc] = (
|
||||
("direction" | directionCodec) ::
|
||||
("add" | updateAddHtlcCodec)).as[DirectedHtlc]
|
||||
|
||||
def setCodec[T](codec: Codec[T]): Codec[Set[T]] = Codec[Set[T]](
|
||||
(elems: Set[T]) => listOfN(uint16, codec).encode(elems.toList),
|
||||
(wire: BitVector) => listOfN(uint16, codec).decode(wire).map(_.map(_.toSet))
|
||||
)
|
||||
|
||||
val commitmentSpecCodec: Codec[CommitmentSpec] = (
|
||||
("htlcs" | setCodec(htlcCodec)) ::
|
||||
("feeratePerKw" | uint32) ::
|
||||
("toLocalMsat" | uint64) ::
|
||||
("toRemoteMsat" | uint64)).as[CommitmentSpec]
|
||||
|
||||
def outPointCodec: Codec[OutPoint] = variableSizeBytes(uint16, bytes.xmap(d => OutPoint.read(d.toArray), d => ByteVector(OutPoint.write(d).data)))
|
||||
|
||||
def txOutCodec: Codec[TxOut] = variableSizeBytes(uint16, bytes.xmap(d => TxOut.read(d.toArray), d => ByteVector(TxOut.write(d).data)))
|
||||
|
||||
def txCodec: Codec[Transaction] = variableSizeBytes(uint16, bytes.xmap(d => Transaction.read(d.toArray), d => ByteVector(Transaction.write(d).data)))
|
||||
|
||||
val inputInfoCodec: Codec[InputInfo] = (
|
||||
("outPoint" | outPointCodec) ::
|
||||
("txOut" | txOutCodec) ::
|
||||
("redeemScript" | varsizebinarydata)).as[InputInfo]
|
||||
|
||||
val txWithInputInfoCodec: Codec[TransactionWithInputInfo] = discriminated[TransactionWithInputInfo].by(uint16)
|
||||
.typecase(0x01, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx])
|
||||
.typecase(0x02, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec) :: ("paymentHash" | binarydata(32))).as[HtlcSuccessTx])
|
||||
.typecase(0x03, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcTimeoutTx])
|
||||
.typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcSuccessTx])
|
||||
.typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcTimeoutTx])
|
||||
.typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx])
|
||||
.typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimDelayedOutputTx])
|
||||
.typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx])
|
||||
.typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx])
|
||||
.typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClosingTx])
|
||||
|
||||
val htlcTxAndSigsCodec: Codec[HtlcTxAndSigs] = (
|
||||
("txinfo" | txWithInputInfoCodec) ::
|
||||
("localSig" | varsizebinarydata) ::
|
||||
("remoteSig" | varsizebinarydata)).as[HtlcTxAndSigs]
|
||||
|
||||
val publishableTxsCodec: Codec[PublishableTxs] = (
|
||||
("commitTx" | (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[CommitTx]) ::
|
||||
("htlcTxsAndSigs" | listOfN(uint16, htlcTxAndSigsCodec))).as[PublishableTxs]
|
||||
|
||||
val localCommitCodec: Codec[LocalCommit] = (
|
||||
("index" | uint64) ::
|
||||
("spec" | commitmentSpecCodec) ::
|
||||
("publishableTxs" | publishableTxsCodec)).as[LocalCommit]
|
||||
|
||||
val remoteCommitCodec: Codec[RemoteCommit] = (
|
||||
("index" | uint64) ::
|
||||
("spec" | commitmentSpecCodec) ::
|
||||
("txid" | binarydata(32)) ::
|
||||
("remotePerCommitmentPoint" | point)).as[RemoteCommit]
|
||||
|
||||
val updateMessageCodec: Codec[UpdateMessage] = lightningMessageCodec.narrow(f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)
|
||||
|
||||
val localChangesCodec: Codec[LocalChanges] = (
|
||||
("proposed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("signed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("acked" | listOfN(uint16, updateMessageCodec))).as[LocalChanges]
|
||||
|
||||
val remoteChangesCodec: Codec[RemoteChanges] = (
|
||||
("proposed" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("acked" | listOfN(uint16, updateMessageCodec)) ::
|
||||
("signed" | listOfN(uint16, updateMessageCodec))).as[RemoteChanges]
|
||||
|
||||
val waitingForRevocationCodec: Codec[WaitingForRevocation] = (
|
||||
("nextRemoteCommit" | remoteCommitCodec) ::
|
||||
("sent" | commitSigCodec) ::
|
||||
("sentAfterLocalCommitIndex" | uint64) ::
|
||||
("reSignAsap" | bool)).as[WaitingForRevocation]
|
||||
|
||||
val relayedCodec: Codec[Relayed] = (
|
||||
("originChannelId" | binarydata(32)) ::
|
||||
("originHtlcId" | int64) ::
|
||||
("amountMsatIn" | uint64) ::
|
||||
("amountMsatOut" | uint64)).as[Relayed]
|
||||
|
||||
val originCodec: Codec[Origin] = discriminated[Origin].by(uint16)
|
||||
.typecase(0x01, provide(Local(None)))
|
||||
.typecase(0x02, relayedCodec)
|
||||
|
||||
val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec)
|
||||
|
||||
val originsMapCodec: Codec[Map[Long, Origin]] = Codec[Map[Long, Origin]](
|
||||
(map: Map[Long, Origin]) => originsListCodec.encode(map.toList),
|
||||
(wire: BitVector) => originsListCodec.decode(wire).map(_.map(_.toMap))
|
||||
)
|
||||
|
||||
val commitmentsCodec: Codec[Commitments] = (
|
||||
("localParams" | localParamsCodec) ::
|
||||
("remoteParams" | remoteParamsCodec) ::
|
||||
("channelFlags" | byte) ::
|
||||
("localCommit" | localCommitCodec) ::
|
||||
("remoteCommit" | remoteCommitCodec) ::
|
||||
("localChanges" | localChangesCodec) ::
|
||||
("remoteChanges" | remoteChangesCodec) ::
|
||||
("localNextHtlcId" | uint64) ::
|
||||
("remoteNextHtlcId" | uint64) ::
|
||||
("originChannels" | originsMapCodec) ::
|
||||
("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, point)) ::
|
||||
("commitInput" | inputInfoCodec) ::
|
||||
("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) ::
|
||||
("channelId" | binarydata(32))).as[Commitments]
|
||||
|
||||
val localCommitPublishedCodec: Codec[LocalCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainDelayedOutputTx" | optional(bool, txCodec)) ::
|
||||
("htlcSuccessTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("claimHtlcDelayedTx" | listOfN(uint16, txCodec)) ::
|
||||
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[LocalCommitPublished]
|
||||
|
||||
val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainOutputTx" | optional(bool, txCodec)) ::
|
||||
("claimHtlcSuccessTxs" | listOfN(uint16, txCodec)) ::
|
||||
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[RemoteCommitPublished]
|
||||
|
||||
val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = (
|
||||
("commitTx" | txCodec) ::
|
||||
("claimMainOutputTx" | optional(bool, txCodec)) ::
|
||||
("mainPenaltyTx" | optional(bool, txCodec)) ::
|
||||
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcPenaltyTxs" | listOfN(uint16, txCodec)) ::
|
||||
("spent" | provide(Map.empty[OutPoint, BinaryData]))).as[RevokedCommitPublished]
|
||||
|
||||
val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("deferred" | optional(bool, fundingLockedCodec)) ::
|
||||
("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED]
|
||||
|
||||
val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED]
|
||||
|
||||
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("shortChannelId" | optional(bool, uint64)) ::
|
||||
("localAnnouncementSignatures" | optional(bool, announcementSignaturesCodec)) ::
|
||||
("localShutdown" | optional(bool, shutdownCodec)) ::
|
||||
("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL]
|
||||
|
||||
val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("localShutdown" | shutdownCodec) ::
|
||||
("remoteShutdown" | shutdownCodec)).as[DATA_SHUTDOWN]
|
||||
|
||||
val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("localShutdown" | shutdownCodec) ::
|
||||
("remoteShutdown" | shutdownCodec) ::
|
||||
("localClosingSigned" | closingSignedCodec)).as[DATA_NEGOTIATING]
|
||||
|
||||
val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("mutualClosePublished" | optional(bool, txCodec)) ::
|
||||
("localCommitPublished" | optional(bool, localCommitPublishedCodec)) ::
|
||||
("remoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
|
||||
("nextRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) ::
|
||||
("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING]
|
||||
|
||||
val stateDataCodec: Codec[HasCommitments] = ("version" | constant(0x00)) ~> discriminated[HasCommitments].by(uint16)
|
||||
.typecase(0x01, DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec)
|
||||
.typecase(0x02, DATA_WAIT_FOR_FUNDING_LOCKED_Codec)
|
||||
.typecase(0x03, DATA_NORMAL_Codec)
|
||||
.typecase(0x04, DATA_SHUTDOWN_Codec)
|
||||
.typecase(0x05, DATA_NEGOTIATING_Codec)
|
||||
.typecase(0x06, DATA_CLOSING_Codec)
|
||||
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound, codecs}
|
||||
|
||||
/**
|
||||
*
|
||||
* REMOVE THIS A NEW VERSION OF SCODEC IS RELEASED THAT INCLUDES CHANGES MADE IN
|
||||
* https://github.com/scodec/scodec/pull/99/files
|
||||
*
|
||||
* Created by PM on 02/06/2017.
|
||||
*/
|
||||
final class FixedSizeStrictCodec[A](size: Long, codec: Codec[A]) extends Codec[A] {
|
||||
|
||||
override def sizeBound = SizeBound.exact(size)
|
||||
|
||||
override def encode(a: A) = for {
|
||||
encoded <- codec.encode(a)
|
||||
result <- {
|
||||
if (encoded.size != size)
|
||||
Attempt.failure(Err(s"[$a] requires ${encoded.size} bits but field is fixed size of exactly $size bits"))
|
||||
else
|
||||
Attempt.successful(encoded.padTo(size))
|
||||
}
|
||||
} yield result
|
||||
|
||||
override def decode(buffer: BitVector) = {
|
||||
if (buffer.size == size) {
|
||||
codec.decode(buffer.take(size)) map { res =>
|
||||
DecodeResult(res.value, buffer.drop(size))
|
||||
}
|
||||
} else {
|
||||
Attempt.failure(Err(s"expected exactly $size bits but got ${buffer.size} bits"))
|
||||
}
|
||||
}
|
||||
|
||||
override def toString = s"fixedSizeBitsStrict($size, $codec)"
|
||||
}
|
||||
|
||||
object FixedSizeStrictCodec {
|
||||
/**
|
||||
* Encodes by returning the supplied byte vector if its length is `size` bytes, otherwise returning error;
|
||||
* decodes by taking `size * 8` bits from the supplied bit vector and converting to a byte vector.
|
||||
*
|
||||
* @param size number of bits to encode/decode
|
||||
* @group bits
|
||||
*/
|
||||
def bytesStrict(size: Int): Codec[ByteVector] = new Codec[ByteVector] {
|
||||
private val codec = new FixedSizeStrictCodec(size * 8L, codecs.bits).xmap[ByteVector](_.toByteVector, _.toBitVector)
|
||||
|
||||
def sizeBound = codec.sizeBound
|
||||
|
||||
def encode(b: ByteVector) = codec.encode(b)
|
||||
|
||||
def decode(b: BitVector) = codec.decode(b)
|
||||
|
||||
override def toString = s"bytesStrict($size)"
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Protocol
|
||||
import fr.acinq.eclair.Features._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by PM on 27/01/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FeaturesSpec extends FunSuite {
|
||||
|
||||
test("'initial_routing_sync' feature") {
|
||||
assert(initialRoutingSync("08"))
|
||||
}
|
||||
|
||||
test("features compatibility") {
|
||||
assert(!areSupported(Protocol.writeUInt64(1L << INITIAL_ROUTING_SYNC_BIT_MANDATORY, ByteOrder.BIG_ENDIAN)))
|
||||
assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN)))
|
||||
assert(areSupported("14") == false)
|
||||
assert(areSupported("0141") == false)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Created by PM on 06/07/2017.
|
||||
*/
|
||||
class TestWallet extends EclairWallet {
|
||||
|
||||
override def getBalance: Future[Satoshi] = ???
|
||||
|
||||
override def getFinalAddress: Future[String] = Future.successful("2MsRZ1asG6k94m6GYUufDGaZJMoJ4EV5JKs")
|
||||
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
|
||||
Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw))
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = Future.successful(true)
|
||||
|
||||
override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true)
|
||||
}
|
||||
|
||||
object TestWallet {
|
||||
|
||||
def makeDummyFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): MakeFundingTxResponse = {
|
||||
val fundingTx = Transaction(version = 2,
|
||||
txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
MakeFundingTxResponse(fundingTx, 0)
|
||||
}
|
||||
|
||||
def malleateTx(tx: Transaction): Transaction = {
|
||||
val inputs1 = tx.txIn.map(input => Script.parse(input.signatureScript) match {
|
||||
case OP_PUSHDATA(sig, _) :: OP_PUSHDATA(pub, _) :: Nil if pub.length == 33 && Try(Crypto.decodeSignature(sig)).isSuccess =>
|
||||
val (r, s) = Crypto.decodeSignature(sig)
|
||||
val s1 = Crypto.curve.getN.subtract(s)
|
||||
val sig1 = Crypto.encodeSignature(r, s1)
|
||||
input.copy(signatureScript = Script.write(OP_PUSHDATA(sig1) :: OP_PUSHDATA(pub) :: Nil))
|
||||
})
|
||||
val tx1 = tx.copy(txIn = inputs1)
|
||||
tx1
|
||||
}
|
||||
}
|
||||
@ -1,215 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
|
||||
import fr.acinq.eclair.blockchain.{PublishAsap, WatchConfirmed, WatchEventConfirmed, WatchSpent}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.sys.process.{Process, _}
|
||||
import scala.util.Random
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/bitcoinj-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BitcoinjSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
}
|
||||
|
||||
ignore("bitcoinj wallet commit") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 60 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val fundingPubkeyScript1 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result1 = Await.result(wallet.makeFundingTx(fundingPubkeyScript1, Satoshi(10000L), 20000), 10 seconds)
|
||||
val fundingPubkeyScript2 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result2 = Await.result(wallet.makeFundingTx(fundingPubkeyScript2, Satoshi(10000L), 20000), 10 seconds)
|
||||
|
||||
assert(Await.result(wallet.commit(result1.fundingTx), 10 seconds) == true)
|
||||
assert(Await.result(wallet.commit(result2.fundingTx), 10 seconds) == false)
|
||||
|
||||
}
|
||||
|
||||
/*def ticket() = {
|
||||
val wallet: Wallet = ???
|
||||
|
||||
def makeTx(amount: Coin, script: BitcoinjScript): Transaction = {
|
||||
val tx = new Transaction(wallet.getParams)
|
||||
tx.addOutput(amount, script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
tx
|
||||
}
|
||||
|
||||
val tx1 = makeTx(amount1, script1)
|
||||
val tx2 = makeTx(amount2, script2)
|
||||
|
||||
// everything is fine until here, and as expected tx1 and tx2 spend the same input
|
||||
|
||||
wallet.maybeCommitTx(tx1) // returns true as expected
|
||||
wallet.maybeCommitTx(tx2) // returns true! how come?
|
||||
}*/
|
||||
|
||||
ignore("manual publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
|
||||
logger.info(s"waiting for confirmation of ${result.fundingTx.txid}")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
|
||||
ignore("multiple publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
def send() = {
|
||||
val count = Random.nextInt(20)
|
||||
val listeners = (0 to count).map {
|
||||
case i =>
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
(result.fundingTx.txid, listener)
|
||||
}
|
||||
system.scheduler.scheduleOnce(2 seconds, new Runnable {
|
||||
override def run() = {
|
||||
logger.info(s"generating one block")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 3))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
})
|
||||
for ((txid, listener) <- listeners) {
|
||||
logger.info(s"waiting for confirmation of $txid")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
}
|
||||
|
||||
for (i <- 0 to 10) send()
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ElectrumClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with Logging with BeforeAndAfterAll {
|
||||
|
||||
import ElectrumClient._
|
||||
|
||||
var client: ActorRef = _
|
||||
val probe = TestProbe()
|
||||
val referenceTx = Transaction.read("0200000003947e307df3ab452d23f02b5a65f4ada1804ee733e168e6197b0bd6cc79932b6c010000006a473044022069346ec6526454a481690a3664609f9e8032c34553015cfa2e9b25ebb420a33002206998f21a2aa771ad92a0c1083f4181a3acdb0d42ca51d01be1309da2ffb9cecf012102b4568cc6ee751f6d39f4a908b1fcffdb878f5f784a26a48c0acb0acff9d88e3bfeffffff966d9d969cd5f95bfd53003a35fcc1a50f4fb51f211596e6472583fdc5d38470000000006b4830450221009c9757515009c5709b5b678d678185202b817ef9a69ffb954144615ab11762210220732216384da4bf79340e9c46d0effba6ba92982cca998adfc3f354cec7715f800121035f7c3e077108035026f4ebd5d6ca696ef088d4f34d45d94eab4c41202ec74f9bfefffffff8d5062f5b04455c6cfa7e3f250e5a4fb44308ba2b86baf77f9ad0d782f57071010000006a47304402207f9f7dd91fe537a26d5554105977e3949a5c8c4ef53a6a3bff6da2d36eff928f02202b9427bef487a1825fd0c3c6851d17d5f19e6d73dfee22bf06db591929a2044d012102b4568cc6ee751f6d39f4a908b1fcffdb878f5f784a26a48c0acb0acff9d88e3bfeffffff02809698000000000017a914c82753548fdf4be1c3c7b14872c90b5198e67eaa876e642500000000001976a914e2365ec29471b3e271388b22eadf0e7f54d307a788ac6f771200")
|
||||
val scriptHash: BinaryData = Crypto.sha256(referenceTx.txOut(0).publicKeyScript).reverse
|
||||
|
||||
override protected def beforeAll(): Unit = {
|
||||
val stream = classOf[ElectrumClientSpec].getResourceAsStream("/electrum/servers_testnet.json")
|
||||
val addresses = ElectrumClient.readServerAddresses(stream)
|
||||
stream.close()
|
||||
client = system.actorOf(Props(new ElectrumClient(addresses)), "electrum-client")
|
||||
}
|
||||
|
||||
override protected def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
|
||||
test("connect to an electrumx testnet server") {
|
||||
probe.send(client, AddStatusListener(probe.ref))
|
||||
probe.expectMsg(5 seconds, ElectrumReady)
|
||||
}
|
||||
|
||||
test("get transaction") {
|
||||
probe.send(client, GetTransaction("c5efb5cbd35a44ba956b18100be0a91c9c33af4c7f31be20e33741d95f04e202"))
|
||||
val GetTransactionResponse(tx) = probe.expectMsgType[GetTransactionResponse]
|
||||
assert(tx.txid == BinaryData("c5efb5cbd35a44ba956b18100be0a91c9c33af4c7f31be20e33741d95f04e202"))
|
||||
}
|
||||
|
||||
test("get merkle tree") {
|
||||
probe.send(client, GetMerkle("c5efb5cbd35a44ba956b18100be0a91c9c33af4c7f31be20e33741d95f04e202", 1210223L))
|
||||
val response = probe.expectMsgType[GetMerkleResponse]
|
||||
assert(response.txid == BinaryData("c5efb5cbd35a44ba956b18100be0a91c9c33af4c7f31be20e33741d95f04e202"))
|
||||
assert(response.block_height == 1210223L)
|
||||
assert(response.pos == 28)
|
||||
assert(response.root == BinaryData("fb0234a21e96913682bc4108bcf72b67fb5d2dd680875b7e4671c03ccf523a20"))
|
||||
}
|
||||
|
||||
test("header subscription") {
|
||||
val probe1 = TestProbe()
|
||||
probe1.send(client, HeaderSubscription(probe1.ref))
|
||||
val HeaderSubscriptionResponse(header) = probe1.expectMsgType[HeaderSubscriptionResponse]
|
||||
logger.info(s"received header for block ${header.block_hash}")
|
||||
}
|
||||
|
||||
test("scripthash subscription") {
|
||||
val probe1 = TestProbe()
|
||||
probe1.send(client, ScriptHashSubscription(scriptHash, probe1.ref))
|
||||
val ScriptHashSubscriptionResponse(scriptHash1, status) = probe1.expectMsgType[ScriptHashSubscriptionResponse]
|
||||
assert(status != "")
|
||||
}
|
||||
|
||||
test("get scripthash history") {
|
||||
probe.send(client, GetScriptHashHistory(scriptHash))
|
||||
val GetScriptHashHistoryResponse(scriptHash1, history) = probe.expectMsgType[GetScriptHashHistoryResponse]
|
||||
assert(history.contains((TransactionHistoryItem(1210224, "3903726806aa044fe59f40e42eed71bded068b43aaa9e2d716e38b7825412de0"))))
|
||||
}
|
||||
|
||||
test("list script unspents") {
|
||||
probe.send(client, ScriptHashListUnspent(scriptHash))
|
||||
val ScriptHashListUnspentResponse(scriptHash1, unspents) = probe.expectMsgType[ScriptHashListUnspentResponse]
|
||||
assert(unspents.contains(UnspentItem("3903726806aa044fe59f40e42eed71bded068b43aaa9e2d716e38b7825412de0", 0, 10000000L, 1210224L)))
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.{JInt, JValue}
|
||||
import org.json4s.jackson.JsonMethods
|
||||
import org.junit.Ignore
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
@Ignore
|
||||
class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
require(System.getProperty("buildDirectory") != null, "please define system property buildDirectory")
|
||||
require(System.getProperty("electrumxPath") != null, "please define system property electrumxPath")
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.15.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
val PATH_ELECTRUMX_DBDIR = new File(INTEGRATION_TMP_DIR, "electrumx-db")
|
||||
val PATH_ELECTRUMX = new File(System.getProperty("electrumxPath"))
|
||||
|
||||
var bitcoind: Process = _
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = _
|
||||
var bitcoincli: ActorRef = _
|
||||
|
||||
var elecxtrumx: Process = _
|
||||
var electrumClient: ActorRef = _
|
||||
|
||||
case class BitcoinReq(method: String, params: Seq[Any] = Nil)
|
||||
|
||||
override protected def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method, Nil) =>
|
||||
bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) =>
|
||||
bitcoinrpcclient.invoke(method, params: _*) pipeTo sender
|
||||
}
|
||||
}))
|
||||
Files.createDirectories(PATH_ELECTRUMX_DBDIR.toPath)
|
||||
startBitcoind
|
||||
logger.info(s"generating initial blocks...")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500 :: Nil))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
startElectrum
|
||||
electrumClient = system.actorOf(Props(new ElectrumClient(Seq(new InetSocketAddress("localhost", 51001)))))
|
||||
sender.send(electrumClient, ElectrumClient.AddStatusListener(sender.ref))
|
||||
sender.expectMsg(3 seconds, ElectrumClient.ElectrumReady)
|
||||
}
|
||||
|
||||
override protected def afterAll(): Unit = {
|
||||
logger.info(s"stopping bitcoind")
|
||||
stopBitcoind
|
||||
bitcoind.destroy()
|
||||
logger.info(s"stopping electrumx")
|
||||
elecxtrumx.destroy()
|
||||
}
|
||||
|
||||
def startBitcoind: Unit = {
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"bitcoind is ready")
|
||||
}
|
||||
|
||||
def stopBitcoind: Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
bitcoind.exitValue()
|
||||
}
|
||||
|
||||
def restartBitcoind: Unit = {
|
||||
stopBitcoind
|
||||
startBitcoind
|
||||
}
|
||||
|
||||
def startElectrum: Unit = {
|
||||
elecxtrumx = Process(s"$PATH_ELECTRUMX/electrumx_server.py",
|
||||
None,
|
||||
"DB_DIRECTORY" -> PATH_ELECTRUMX_DBDIR.getAbsolutePath,
|
||||
"DAEMON_URL" -> "foo:bar@localhost:28332",
|
||||
"COIN" -> "BitcoinSegwit",
|
||||
"NET" -> "regtest",
|
||||
"TCP_PORT" -> "51001").run()
|
||||
|
||||
logger.info(s"waiting for electrumx to initialize...")
|
||||
awaitCond({
|
||||
val result = s"$PATH_ELECTRUMX/electrumx_rpc.py getinfo".!!
|
||||
Try(JsonMethods.parse(result) \ "daemon_height") match {
|
||||
case Success(JInt(value)) if value.intValue() == 500 => true
|
||||
case _ => false
|
||||
}
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
|
||||
import fr.acinq.bitcoin._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ElectrumWalletBasicSpec extends FunSuite {
|
||||
|
||||
import ElectrumWallet._
|
||||
|
||||
val swipeRange = 10
|
||||
val dustLimit = 546 satoshi
|
||||
val feeRatePerKw = 20000
|
||||
val minimumFee = Satoshi(2000)
|
||||
|
||||
val master = DeterministicWallet.generate(BinaryData("01" * 32))
|
||||
val accountMaster = accountKey(master)
|
||||
val accountIndex = 0
|
||||
|
||||
val changeMaster = changeKey(master)
|
||||
val changeIndex = 0
|
||||
|
||||
val firstAccountKeys = (0 until 10).map(i => derivePrivateKey(accountMaster, i)).toVector
|
||||
val firstChangeKeys = (0 until 10).map(i => derivePrivateKey(changeMaster, i)).toVector
|
||||
|
||||
val params = ElectrumWallet.WalletParameters(Block.RegtestGenesisBlock.hash)
|
||||
|
||||
val state = Data(params, ElectrumClient.Header.RegtestGenesisHeader, firstAccountKeys, firstChangeKeys)
|
||||
val unspents = Map(
|
||||
computeScriptHashFromPublicKey(state.accountKeys(0).publicKey) -> Set(ElectrumClient.UnspentItem("01" * 32, 0, 1 * Satoshi(Coin).toLong, 100)),
|
||||
computeScriptHashFromPublicKey(state.accountKeys(1).publicKey) -> Set(ElectrumClient.UnspentItem("02" * 32, 0, 2 * Satoshi(Coin).toLong, 100)),
|
||||
computeScriptHashFromPublicKey(state.accountKeys(2).publicKey) -> Set(ElectrumClient.UnspentItem("03" * 32, 0, 3 * Satoshi(Coin).toLong, 100))
|
||||
)
|
||||
|
||||
test("compute addresses") {
|
||||
val priv = PrivateKey.fromBase58("cRumXueoZHjhGXrZWeFoEBkeDHu2m8dW5qtFBCqSAt4LDR2Hnd8Q", Base58.Prefix.SecretKeyTestnet)
|
||||
assert(Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, priv.publicKey.hash160) == "ms93boMGZZjvjciujPJgDAqeR86EKBf9MC")
|
||||
assert(segwitAddress(priv) == "2MscvqgGXMTYJNAY3owdUtgWJaxPUjH38Cx")
|
||||
}
|
||||
|
||||
test("implement BIP49") {
|
||||
val mnemonics = "pizza afraid guess romance pair steel record jazz rubber prison angle hen heart engage kiss visual helmet twelve lady found between wave rapid twist".split(" ")
|
||||
val seed = MnemonicCode.toSeed(mnemonics, "")
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
|
||||
val accountMaster = accountKey(master)
|
||||
val firstKey = derivePrivateKey(accountMaster, 0)
|
||||
assert(segwitAddress(firstKey) === "2MxJejujQJRRJdbfTKNQQ94YCnxJwRaE7yo")
|
||||
}
|
||||
|
||||
ignore("complete transactions (enough funds)") {
|
||||
val state1 = state.copy(status = (state.accountKeys ++ state.changeKeys).map(key => computeScriptHashFromPublicKey(key.publicKey) -> "").toMap)
|
||||
|
||||
val pub = PrivateKey(BinaryData("01" * 32), compressed = true).publicKey
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(pub)) :: Nil, lockTime = 0)
|
||||
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
|
||||
|
||||
val state3 = state2.cancelTransaction(tx1)
|
||||
assert(state3 == state1)
|
||||
|
||||
val state4 = state2.commitTransaction(tx1)
|
||||
assert(state4.utxos.size + tx1.txIn.size == state1.utxos.size)
|
||||
}
|
||||
|
||||
test("complete transactions (insufficient funds)") {
|
||||
val state1 = state.copy(status = (state.accountKeys ++ state.changeKeys).map(key => computeScriptHashFromPublicKey(key.publicKey) -> "").toMap)
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(6 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
|
||||
val e = intercept[IllegalArgumentException] {
|
||||
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
|
||||
}
|
||||
}
|
||||
|
||||
ignore("find what a tx spends from us") {
|
||||
val state1 = state.copy(status = (state.accountKeys ++ state.changeKeys).map(key => computeScriptHashFromPublicKey(key.publicKey) -> "").toMap)
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
|
||||
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
|
||||
|
||||
val pubkeys = tx1.txIn.map(extractPubKeySpentFrom).flatten
|
||||
val utxos1 = state2.utxos.filter(utxo => pubkeys.contains(utxo.key.publicKey))
|
||||
val utxos2 = state2.utxos.filter(utxo => tx1.txIn.map(_.outPoint).contains(utxo.outPoint))
|
||||
println(pubkeys)
|
||||
}
|
||||
|
||||
ignore("find what a tx sends to us") {
|
||||
val state1 = state.copy(status = (state.accountKeys ++ state.changeKeys).map(key => computeScriptHashFromPublicKey(key.publicKey) -> "").toMap)
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0)
|
||||
val (state2, tx1) = state1.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, false)
|
||||
|
||||
val pubSpent = tx1.txIn.map(extractPubKeySpentFrom).flatten
|
||||
val utxos1 = state2.utxos.filter(utxo => pubSpent.contains(utxo.key.publicKey))
|
||||
val utxos2 = state2.utxos.filter(utxo => tx1.txIn.map(_.outPoint).contains(utxo.outPoint))
|
||||
println(pubSpent)
|
||||
}
|
||||
}
|
||||
@ -1,300 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.{ActorRef, Props}
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{AddStatusListener, BroadcastTransaction, BroadcastTransactionResponse}
|
||||
import org.json4s.JsonAST._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
class ElectrumWalletSpec extends IntegrationSpec {
|
||||
|
||||
import ElectrumWallet._
|
||||
|
||||
val entropy = BinaryData("01" * 32)
|
||||
val mnemonics = MnemonicCode.toMnemonics(entropy)
|
||||
val seed = MnemonicCode.toSeed(mnemonics, "")
|
||||
logger.info(s"mnemonic codes for our wallet: $mnemonics")
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
var wallet: ActorRef = _
|
||||
|
||||
test("wait until wallet is ready") {
|
||||
wallet = system.actorOf(Props(new ElectrumWallet(mnemonics, electrumClient, WalletParameters(Block.RegtestGenesisBlock.hash, minimumFee = Satoshi(5000)))), "wallet")
|
||||
val probe = TestProbe()
|
||||
awaitCond({
|
||||
probe.send(wallet, GetData)
|
||||
val GetDataResponse(state) = probe.expectMsgType[GetDataResponse]
|
||||
state.status.size == state.accountKeys.size + state.changeKeys.size
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
logger.info(s"wallet is ready")
|
||||
}
|
||||
|
||||
ignore("receive funds") {
|
||||
val probe = TestProbe()
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
logger.info(s"sending 1 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
unconfirmed == Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
confirmed == Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address1) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
logger.info(s"sending 1 btc to $address1")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1 :: 1.0 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
logger.info(s"sending 0.5 btc to $address1")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1 :: 0.5 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
confirmed == Satoshi(250000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("receive 'confidence changed' notification") {
|
||||
val probe = TestProbe()
|
||||
val listener = TestProbe()
|
||||
|
||||
listener.send(wallet, AddStatusListener(listener.ref))
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
|
||||
logger.info(s"sending 1 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
val JString(txid) = probe.expectMsgType[JValue]
|
||||
logger.info(s"$txid send 1 btc to us at $address")
|
||||
|
||||
val TransactionReceived(tx, 0, received, sent, _) = listener.receiveOne(5 seconds)
|
||||
assert(tx.txid === BinaryData(txid))
|
||||
assert(received === Satoshi(100000000))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
confirmed1 - confirmed == Satoshi(100000000L)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
awaitCond({
|
||||
val msg = listener.receiveOne(5 seconds)
|
||||
msg == TransactionConfidenceChanged(BinaryData(txid), 1)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("send money to someone else (we broadcast)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(1 btc, Script.pay2pkh(pubKeyHash)) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
// send it ourselves
|
||||
logger.info(s"sending 1 btc to $address with tx ${tx1.txid}")
|
||||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address :: Nil))
|
||||
val JDouble(value) = probe.expectMsgType[JValue]
|
||||
value == 1.0
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed - Btc(1) && confirmed1 > confirmed - Btc(1) - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("send money to ourselves (we broadcast)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
val (Base58.Prefix.ScriptAddressTestnet, scriptHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(1 btc, OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
// send it ourselves
|
||||
logger.info(s"sending 1 btc to $address with tx ${tx1.txid}")
|
||||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed && confirmed1 > confirmed - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
ignore("handle reorgs (pending receive)") {
|
||||
val probe = TestProbe()
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
|
||||
probe.send(wallet, GetCurrentReceiveAddress)
|
||||
val GetCurrentReceiveAddressResponse(address) = probe.expectMsgType[GetCurrentReceiveAddressResponse]
|
||||
|
||||
// send money to our receive address
|
||||
logger.info(s"sending 0.7 btc to $address")
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 0.7 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
// generate 1 block
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
val JArray(List(JString(blockId))) = probe.expectMsgType[JValue]
|
||||
|
||||
// wait until our balance has been updated
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed + Btc(0.7)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
// now invalidate the last block
|
||||
probe.send(bitcoincli, BitcoinReq("invalidateblock", blockId :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
// and restart bitcoind, which should remove pending wallet txs
|
||||
// bitcoind was started with -zapwallettxes=2
|
||||
stopBitcoind
|
||||
Thread.sleep(2000)
|
||||
startBitcoind
|
||||
Thread.sleep(2000)
|
||||
|
||||
|
||||
// generate 2 new blocks. the tx that sent us money is no longer there,
|
||||
// the corresponding utxo should have been removed and our balance should
|
||||
// be back to what it was before
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
val reorg = s"$PATH_ELECTRUMX/electrumx_rpc.py reorg 2".!!
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
ignore("handle reorgs (pending send)") {
|
||||
val probe = TestProbe()
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
val (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) = Base58Check.decode(address)
|
||||
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed, unconfirmed) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"we start with a balance of $confirmed")
|
||||
|
||||
// create a tx that sends money to Bitcoin Core's address
|
||||
val amount = 0.5 btc
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, Script.pay2pkh(pubKeyHash)) :: Nil, lockTime = 0L)
|
||||
probe.send(wallet, CompleteTransaction(tx, 20000))
|
||||
val CompleteTransactionResponse(tx1, None) = probe.expectMsgType[CompleteTransactionResponse]
|
||||
|
||||
// send it ourselves
|
||||
logger.info(s"sending $amount to $address with tx ${tx1.txid}")
|
||||
probe.send(wallet, BroadcastTransaction(tx1))
|
||||
val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse]
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 1 :: Nil))
|
||||
val JArray(List(JString(blockId))) = probe.expectMsgType[JValue]
|
||||
|
||||
awaitCond({
|
||||
probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address :: Nil))
|
||||
val JDouble(value) = probe.expectMsgType[JValue]
|
||||
value == amount.amount.toDouble
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 < confirmed - amount && confirmed1 > confirmed - amount - Satoshi(50000)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
|
||||
// now invalidate the last block
|
||||
probe.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val JInt(count) = probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("invalidateblock", blockId :: Nil))
|
||||
val foo = probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val JInt(count1) = probe.expectMsgType[JValue]
|
||||
|
||||
// and restart bitcoind, which should remove pending wallet txs
|
||||
// bitcoind was started with -zapwallettxes=2
|
||||
stopBitcoind
|
||||
Thread.sleep(2000)
|
||||
startBitcoind
|
||||
Thread.sleep(2000)
|
||||
|
||||
// generate 2 new blocks. the tx that sent us money is no longer there,
|
||||
// the corresponding utxo should have been removed and our balance should
|
||||
// be back to what it was before
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
val reorg = s"$PATH_ELECTRUMX/electrumx_rpc.py reorg 2".!!
|
||||
|
||||
awaitCond({
|
||||
probe.send(wallet, GetBalance)
|
||||
val GetBalanceResponse(confirmed1, unconfirmed1) = probe.expectMsgType[GetBalanceResponse]
|
||||
logger.debug(s"current balance is $confirmed1")
|
||||
confirmed1 == confirmed
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.electrum
|
||||
|
||||
import akka.actor.Props
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Base58, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain.{WatchConfirmed, WatchEventConfirmed, WatchEventSpent, WatchSpent}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import org.json4s.JsonAST.{JArray, JString, JValue}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class ElectrumWatcherSpec extends IntegrationSpec {
|
||||
test("watch for confirmed transactions") {
|
||||
val probe = TestProbe()
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient)))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
println(address)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid :: Nil))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut(0).publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK))
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 3 :: Nil))
|
||||
listener.expectNoMsg(1 second)
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds)
|
||||
system.stop(watcher)
|
||||
}
|
||||
|
||||
test("watch for spent transactions") {
|
||||
val probe = TestProbe()
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(electrumClient)))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
|
||||
val JString(address) = probe.expectMsgType[JValue]
|
||||
println(address)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("dumpprivkey", address :: Nil))
|
||||
val JString(wif) = probe.expectMsgType[JValue]
|
||||
val priv = PrivateKey.fromBase58(wif, Base58.Prefix.SecretKeyTestnet)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address :: 1.0 :: Nil))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid :: Nil))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
// find the output for the address we generated and create a tx that spends it
|
||||
val pos = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2pkh(priv.publicKey)))
|
||||
assert(pos != -1)
|
||||
val spendingTx = {
|
||||
val tmp = Transaction(version = 2,
|
||||
txIn = TxIn(OutPoint(tx, pos), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(tx.txOut(pos).amount - Satoshi(1000), publicKeyScript = Script.pay2pkh(priv.publicKey)) :: Nil,
|
||||
lockTime = 0)
|
||||
val sig = Transaction.signInput(tmp, 0, tx.txOut(pos).publicKeyScript, SIGHASH_ALL, tx.txOut(pos).amount, SigVersion.SIGVERSION_BASE, priv)
|
||||
val signedTx = tmp.updateSigScript(0, OP_PUSHDATA(sig) :: OP_PUSHDATA(priv.publicKey.toBin) :: Nil)
|
||||
Transaction.correctlySpends(signedTx, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
signedTx
|
||||
}
|
||||
|
||||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchSpent(listener.ref, tx.txid, pos, tx.txOut(pos).publicKeyScript, BITCOIN_FUNDING_SPENT))
|
||||
listener.expectNoMsg(1 second)
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", Transaction.write(spendingTx).toString :: Nil))
|
||||
probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("generate", 2 :: Nil))
|
||||
val blocks = probe.expectMsgType[JValue]
|
||||
val JArray(List(JString(block1), JString(block2))) = blocks
|
||||
val spent = listener.expectMsgType[WatchEventSpent](20 seconds)
|
||||
system.stop(watcher)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1,62 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.fee
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.util.Random
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FallbackFeeProviderSpec extends FunSuite {
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
/**
|
||||
* This provider returns a constant value, but fails after ttl tries
|
||||
*
|
||||
* @param ttl
|
||||
* @param feeratesPerByte
|
||||
*/
|
||||
class FailingFeeProvider(ttl: Int, val feeratesPerByte: FeeratesPerByte) extends FeeProvider {
|
||||
var i = 0
|
||||
|
||||
override def getFeerates: Future[FeeratesPerByte] =
|
||||
if (i < ttl) {
|
||||
i = i + 1
|
||||
Future.successful(feeratesPerByte)
|
||||
} else Future.failed(new RuntimeException())
|
||||
}
|
||||
|
||||
def dummyFeerates = FeeratesPerByte(Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000), Random.nextInt(10000))
|
||||
|
||||
def await[T](f: Future[T]): T = Await.result(f, 3 seconds)
|
||||
|
||||
test("fee provider failover") {
|
||||
val provider0 = new FailingFeeProvider(-1, dummyFeerates) // always fails
|
||||
val provider1 = new FailingFeeProvider(1, dummyFeerates) // fails after 1 try
|
||||
val provider3 = new FailingFeeProvider(3, dummyFeerates) // fails after 3 tries
|
||||
val provider5 = new FailingFeeProvider(5, dummyFeerates) // fails after 5 tries
|
||||
val provider7 = new FailingFeeProvider(Int.MaxValue, dummyFeerates) // "never" fails
|
||||
|
||||
val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil)
|
||||
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerByte)
|
||||
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerByte)
|
||||
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerByte)
|
||||
|
||||
assert(await(fallbackFeeProvider.getFeerates) === provider7.feeratesPerByte)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
|
||||
import fr.acinq.eclair.channel.Commitments.msg2String
|
||||
import fr.acinq.eclair.wire.LightningMessage
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* A Fuzzy [[fr.acinq.eclair.Pipe]] which randomly disconnects/reconnects peers.
|
||||
*/
|
||||
class FuzzyPipe(fuzzy: Boolean) extends Actor with Stash with ActorLogging {
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
def receive = {
|
||||
case (a: ActorRef, b: ActorRef) =>
|
||||
unstashAll()
|
||||
context become connected(a, b, 10)
|
||||
|
||||
case _ => stash()
|
||||
}
|
||||
|
||||
def stayOrDisconnect(a: ActorRef, b: ActorRef, countdown: Int) = {
|
||||
if (!fuzzy) context become connected(a, b, countdown - 1) // fuzzy mode disabled, we never disconnect
|
||||
else if (countdown > 1) context become connected(a, b, countdown - 1)
|
||||
else {
|
||||
log.debug("DISCONNECTED")
|
||||
a ! INPUT_DISCONNECTED
|
||||
b ! INPUT_DISCONNECTED
|
||||
context.system.scheduler.scheduleOnce(100 millis, self, 'reconnect)
|
||||
context become disconnected(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
def connected(a: ActorRef, b: ActorRef, countdown: Int): Receive = {
|
||||
case msg: LightningMessage if sender() == a =>
|
||||
log.debug(f"A ---${msg2String(msg)}%-6s--> B")
|
||||
b forward msg
|
||||
stayOrDisconnect(a, b, countdown)
|
||||
case msg: LightningMessage if sender() == b =>
|
||||
log.debug(f"A <--${msg2String(msg)}%-6s--- B")
|
||||
a forward msg
|
||||
stayOrDisconnect(a, b, countdown)
|
||||
}
|
||||
|
||||
def disconnected(a: ActorRef, b: ActorRef): Receive = {
|
||||
case msg: LightningMessage if sender() == a =>
|
||||
// dropped
|
||||
log.info(f"A ---${msg2String(msg)}%-6s-X")
|
||||
case msg: LightningMessage if sender() == b =>
|
||||
// dropped
|
||||
log.debug(f" X-${msg2String(msg)}%-6s--- B")
|
||||
case 'reconnect =>
|
||||
log.debug("RECONNECTED")
|
||||
a ! INPUT_RECONNECTED(self)
|
||||
b ! INPUT_RECONNECTED(self)
|
||||
context become connected(a, b, Random.nextInt(40))
|
||||
}
|
||||
}
|
||||
@ -1,179 +0,0 @@
|
||||
package fr.acinq.eclair.channel
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status}
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.wire._
|
||||
import grizzled.slf4j.Logging
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.Tag
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.collection.immutable.Nil
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 05/07/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Logging {
|
||||
|
||||
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], ActorRef, ActorRef, ActorRef, ActorRef, ActorRef]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val fuzzy = test.tags.contains("fuzzy")
|
||||
val pipe = system.actorOf(Props(new FuzzyPipe(fuzzy)))
|
||||
val alice2blockchain = TestProbe()
|
||||
val bob2blockchain = TestProbe()
|
||||
val paymentHandlerA = system.actorOf(Props(new LocalPaymentHandler(Alice.nodeParams)))
|
||||
val paymentHandlerB = system.actorOf(Props(new LocalPaymentHandler(Bob.nodeParams)))
|
||||
val registerA = TestProbe()
|
||||
val registerB = TestProbe()
|
||||
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, registerA.ref, paymentHandlerA))
|
||||
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, registerB.ref, paymentHandlerB))
|
||||
val router = TestProbe()
|
||||
val wallet = new TestWallet
|
||||
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.id, alice2blockchain.ref, router.ref, relayerA))
|
||||
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.id, bob2blockchain.ref, router.ref, relayerB))
|
||||
within(30 seconds) {
|
||||
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
|
||||
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
|
||||
relayerA ! alice
|
||||
relayerB ! bob
|
||||
// no announcements
|
||||
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte)
|
||||
bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit)
|
||||
pipe ! (alice, bob)
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
alice2blockchain.expectMsgType[WatchConfirmed]
|
||||
bob2blockchain.expectMsgType[WatchSpent]
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
|
||||
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
|
||||
alice2blockchain.expectMsgType[WatchLost]
|
||||
bob2blockchain.expectMsgType[WatchLost]
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
}
|
||||
test((alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))
|
||||
}
|
||||
|
||||
class SenderActor(channel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef, latch: CountDownLatch) extends Actor with ActorLogging {
|
||||
|
||||
// we don't want to be below htlcMinimumMsat
|
||||
val requiredAmount = 1000000
|
||||
|
||||
def buildCmdAdd(paymentHash: BinaryData, dest: PublicKey) = {
|
||||
// allow overpaying (no more than 2 times the required amount)
|
||||
val amount = requiredAmount + Random.nextInt(requiredAmount)
|
||||
val expiry = Globals.blockCount.get().toInt + PaymentLifecycle.defaultMinFinalCltvExpiry
|
||||
PaymentLifecycle.buildCommand(amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1
|
||||
}
|
||||
|
||||
def initiatePayment(stopping: Boolean) =
|
||||
if (stopping) {
|
||||
context stop self
|
||||
} else {
|
||||
paymentHandler ! ReceivePayment(MilliSatoshi(requiredAmount), "One coffee")
|
||||
context become waitingForPaymentRequest
|
||||
}
|
||||
|
||||
initiatePayment(false)
|
||||
|
||||
override def receive: Receive = ???
|
||||
|
||||
def waitingForPaymentRequest: Receive = {
|
||||
case req: PaymentRequest =>
|
||||
channel ! buildCmdAdd(req.paymentHash, req.nodeId)
|
||||
context become waitingForFulfill(false)
|
||||
}
|
||||
|
||||
def waitingForFulfill(stopping: Boolean): Receive = {
|
||||
case u: UpdateFulfillHtlc =>
|
||||
log.info(s"successfully sent htlc #${u.id}")
|
||||
latch.countDown()
|
||||
initiatePayment(stopping)
|
||||
case u: UpdateFailHtlc =>
|
||||
log.warning(s"htlc failed: ${u.id}")
|
||||
initiatePayment(stopping)
|
||||
case Status.Failure(t) =>
|
||||
log.error(s"htlc error: ${t.getMessage}")
|
||||
initiatePayment(stopping)
|
||||
case 'stop =>
|
||||
log.warning(s"stopping...")
|
||||
context become waitingForFulfill(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test("fuzzy test with only one party sending HTLCs", Tag("fuzzy")) {
|
||||
case (alice, bob, _, _, _, _, paymentHandlerB) =>
|
||||
val latch = new CountDownLatch(100)
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch)))
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch)))
|
||||
awaitCond(latch.getCount == 0, max = 2 minutes)
|
||||
assert(alice.stateName == NORMAL || alice.stateName == OFFLINE)
|
||||
assert(bob.stateName == NORMAL || alice.stateName == OFFLINE)
|
||||
}
|
||||
|
||||
test("fuzzy test with both parties sending HTLCs", Tag("fuzzy")) {
|
||||
case (alice, bob, _, _, _, paymentHandlerA, paymentHandlerB) =>
|
||||
val latch = new CountDownLatch(100)
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch)))
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch)))
|
||||
system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch)))
|
||||
system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch)))
|
||||
awaitCond(latch.getCount == 0, max = 2 minutes)
|
||||
assert(alice.stateName == NORMAL || alice.stateName == OFFLINE)
|
||||
assert(bob.stateName == NORMAL || alice.stateName == OFFLINE)
|
||||
}
|
||||
|
||||
test("one party sends lots of htlcs send shutdown") {
|
||||
case (alice, _, _, _, _, _, paymentHandlerB) =>
|
||||
val latch = new CountDownLatch(20)
|
||||
val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) ::
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) ::
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) :: Nil
|
||||
awaitCond(latch.getCount == 0, max = 2 minutes)
|
||||
val sender = TestProbe()
|
||||
awaitCond({
|
||||
sender.send(alice, CMD_CLOSE(None))
|
||||
sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure]) == "ok"
|
||||
}, max = 30 seconds)
|
||||
senders.foreach(_ ! 'stop)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
}
|
||||
|
||||
test("both parties send lots of htlcs send shutdown") {
|
||||
case (alice, bob, _, _, _, paymentHandlerA, paymentHandlerB) =>
|
||||
val latch = new CountDownLatch(30)
|
||||
val senders = system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) ::
|
||||
system.actorOf(Props(new SenderActor(alice, paymentHandlerB, latch))) ::
|
||||
system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) ::
|
||||
system.actorOf(Props(new SenderActor(bob, paymentHandlerA, latch))) :: Nil
|
||||
awaitCond(latch.getCount == 0, max = 2 minutes)
|
||||
val sender = TestProbe()
|
||||
awaitCond({
|
||||
sender.send(alice, CMD_CLOSE(None))
|
||||
val resa = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure])
|
||||
sender.send(bob, CMD_CLOSE(None))
|
||||
val resb = sender.expectMsgAnyClassOf(classOf[String], classOf[Status.Failure])
|
||||
// we only need that one of them succeeds
|
||||
resa == "ok" || resb == "ok"
|
||||
}, max = 30 seconds)
|
||||
senders.foreach(_ ! 'stop)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
package fr.acinq.eclair.channel.states.e
|
||||
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.TestkitBaseClass
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Created by PM on 05/07/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
|
||||
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val setup = init()
|
||||
import setup._
|
||||
within(30 seconds) {
|
||||
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test checks the case where a disconnection occurs *right before* the counterparty receives a new sig
|
||||
*/
|
||||
test("re-send update+sig after first commitment") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
sender.send(alice, CMD_ADD_HTLC(1000000, BinaryData("42" * 32), 400144))
|
||||
val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
// add ->b
|
||||
alice2bob.forward(bob)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
val ab_sig_0 = alice2bob.expectMsgType[CommitSig]
|
||||
// bob doesn't receive the sig
|
||||
|
||||
sender.send(alice, INPUT_DISCONNECTED)
|
||||
sender.send(bob, INPUT_DISCONNECTED)
|
||||
awaitCond(alice.stateName == OFFLINE)
|
||||
awaitCond(bob.stateName == OFFLINE)
|
||||
sender.send(alice, INPUT_RECONNECTED(alice2bob.ref))
|
||||
sender.send(bob, INPUT_RECONNECTED(bob2alice.ref))
|
||||
|
||||
// a didn't receive any update or sig
|
||||
val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0))
|
||||
// b didn't receive the sig
|
||||
val ba_reestablish = bob2alice.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0))
|
||||
|
||||
// reestablish ->b
|
||||
alice2bob.forward(bob, ab_reestablish)
|
||||
// reestablish ->a
|
||||
bob2alice.forward(alice, ba_reestablish)
|
||||
|
||||
// both nodes will send the fundinglocked message because all updates have been cancelled
|
||||
alice2bob.expectMsgType[FundingLocked]
|
||||
bob2alice.expectMsgType[FundingLocked]
|
||||
|
||||
// a will re-send the update and the sig
|
||||
val ab_add_0_re = alice2bob.expectMsg(ab_add_0)
|
||||
val ab_sig_0_re = alice2bob.expectMsg(ab_sig_0)
|
||||
|
||||
// add ->b
|
||||
alice2bob.forward(bob, ab_add_0_re)
|
||||
// sig ->b
|
||||
alice2bob.forward(bob, ab_sig_0_re)
|
||||
|
||||
// and b will reply with a revocation
|
||||
val ba_rev_0 = bob2alice.expectMsgType[RevokeAndAck]
|
||||
// rev ->a
|
||||
bob2alice.forward(alice, ba_rev_0)
|
||||
|
||||
// then b sends a sig
|
||||
bob2alice.expectMsgType[CommitSig]
|
||||
// sig -> a
|
||||
bob2alice.forward(alice)
|
||||
|
||||
// and a answers with a rev
|
||||
alice2bob.expectMsgType[RevokeAndAck]
|
||||
// sig -> a
|
||||
alice2bob.forward(bob)
|
||||
|
||||
alice2bob.expectNoMsg(500 millis)
|
||||
bob2alice.expectNoMsg(500 millis)
|
||||
|
||||
alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localNextHtlcId == 1
|
||||
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* This test checks the case where a disconnection occurs *right after* the counterparty receives a new sig
|
||||
*/
|
||||
test("re-send lost revocation") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
sender.send(alice, CMD_ADD_HTLC(1000000, BinaryData("42" * 32), 400144))
|
||||
val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
// add ->b
|
||||
alice2bob.forward(bob, ab_add_0)
|
||||
|
||||
sender.send(alice, CMD_SIGN)
|
||||
val ab_sig_0 = alice2bob.expectMsgType[CommitSig]
|
||||
// sig ->b
|
||||
alice2bob.forward(bob, ab_sig_0)
|
||||
|
||||
// bob received the sig, but alice didn't receive the revocation
|
||||
val ba_rev_0 = bob2alice.expectMsgType[RevokeAndAck]
|
||||
val ba_sig_0 = bob2alice.expectMsgType[CommitSig]
|
||||
|
||||
bob2alice.expectNoMsg(500 millis)
|
||||
|
||||
sender.send(alice, INPUT_DISCONNECTED)
|
||||
sender.send(bob, INPUT_DISCONNECTED)
|
||||
awaitCond(alice.stateName == OFFLINE)
|
||||
awaitCond(bob.stateName == OFFLINE)
|
||||
sender.send(alice, INPUT_RECONNECTED(alice2bob.ref))
|
||||
sender.send(bob, INPUT_RECONNECTED(bob2alice.ref))
|
||||
|
||||
// a didn't receive the sig
|
||||
val ab_reestablish = alice2bob.expectMsg(ChannelReestablish(ab_add_0.channelId, 1, 0))
|
||||
// b did receive the sig
|
||||
val ba_reestablish = bob2alice.expectMsg(ChannelReestablish(ab_add_0.channelId, 2, 0))
|
||||
|
||||
// reestablish ->b
|
||||
alice2bob.forward(bob, ab_reestablish)
|
||||
// reestablish ->a
|
||||
bob2alice.forward(alice, ba_reestablish)
|
||||
|
||||
// b will re-send the lost revocation
|
||||
val ba_rev_0_re = bob2alice.expectMsg(ba_rev_0)
|
||||
// rev ->a
|
||||
bob2alice.forward(alice, ba_rev_0)
|
||||
|
||||
// and b will attempt a new signature
|
||||
bob2alice.expectMsg(ba_sig_0)
|
||||
|
||||
alice2bob.expectNoMsg(500 millis)
|
||||
bob2alice.expectNoMsg(500 millis)
|
||||
|
||||
alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localNextHtlcId == 1
|
||||
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.spongycastle.util.encoders.Hex
|
||||
|
||||
/**
|
||||
* Created by fabrice on 22/06/17.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitStreamSpec extends FunSuite {
|
||||
|
||||
import BitStream._
|
||||
|
||||
test("add bits") {
|
||||
val bits = BitStream.empty
|
||||
val bits1 = bits.writeBit(One)
|
||||
assert(bits1.bitCount == 1)
|
||||
assert(bits1.isSet(0))
|
||||
assert(Hex.toHexString(bits1.bytes.toArray) == "80")
|
||||
val bits2 = bits1.writeBit(Zero)
|
||||
assert(bits2.bitCount == 2)
|
||||
assert(bits2.isSet(0))
|
||||
assert(!bits2.isSet(1))
|
||||
assert(Hex.toHexString(bits2.bytes.toArray) == "80")
|
||||
val bits3 = bits2.writeBit(One)
|
||||
assert(bits3.bitCount == 3)
|
||||
assert(bits3.isSet(0))
|
||||
assert(!bits3.isSet(1))
|
||||
assert(bits3.isSet(2))
|
||||
assert(bits3.toHexString == "0xa0")
|
||||
assert(bits3.toBinString == "0b101")
|
||||
|
||||
val (bits4, One) = bits3.popBit
|
||||
assert(bits4 == bits2)
|
||||
val (bits5, Zero) = bits4.popBit
|
||||
assert(bits5 == bits1)
|
||||
val (bits6, One) = bits5.popBit
|
||||
assert(bits6 == bits)
|
||||
|
||||
val (bits7, One) = bits3.readBit
|
||||
val (bits8, Zero) = bits7.readBit
|
||||
val (bits9, One) = bits8.readBit
|
||||
assert(bits9.isEmpty)
|
||||
}
|
||||
|
||||
test("add bytes") {
|
||||
val bits = BitStream.empty
|
||||
val bits1 = bits.writeByte(0xb5.toByte)
|
||||
assert(bits1.bitCount == 8)
|
||||
assert(bits1.toHexString == "0xb5")
|
||||
assert(bits1.toBinString == "0b10110101")
|
||||
// b5 = 1100 0101
|
||||
val bits2 = bits1.writeBit(Zero)
|
||||
assert(bits2.bitCount == 9)
|
||||
// 1100 0101 0
|
||||
assert(bits2.toHexString == "0xb500")
|
||||
assert(bits2.toBinString == "0b101101010")
|
||||
val bits3 = bits2.writeBit(One)
|
||||
assert(bits3.bitCount == 10)
|
||||
// 1100 0101 01
|
||||
assert(bits3.toHexString == "0xb540")
|
||||
assert(bits3.toBinString == "0b1011010101")
|
||||
|
||||
// 1011 0101 01xx xxxx
|
||||
// 10xx xxxx and 1101 0101
|
||||
val (bits4, check) = bits3.popByte
|
||||
assert(check == 0xd5.toByte)
|
||||
assert(bits4.toBinString == "0b10")
|
||||
|
||||
val (bits5, check5) = bits3.readByte
|
||||
assert(check5 == 0xb5.toByte)
|
||||
}
|
||||
}
|
||||
@ -1,184 +0,0 @@
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by fabrice on 10/01/17.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SphinxSpec extends FunSuite {
|
||||
|
||||
import Sphinx._
|
||||
import SphinxSpec._
|
||||
|
||||
/*
|
||||
hop_shared_secret[0] = 0x53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66
|
||||
hop_blinding_factor[0] = 0x2ec2e5da605776054187180343287683aa6a51b4b1c04d6dd49c45d8cffb3c36
|
||||
hop_ephemeral_pubkey[0] = 0x02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619
|
||||
|
||||
hop_shared_secret[1] = 0xa6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae
|
||||
hop_blinding_factor[1] = 0xbf66c28bc22e598cfd574a1931a2bafbca09163df2261e6d0056b2610dab938f
|
||||
hop_ephemeral_pubkey[1] = 0x028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e2
|
||||
|
||||
hop_shared_secret[2] = 0x3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc
|
||||
hop_blinding_factor[2] = 0xa1f2dadd184eb1627049673f18c6325814384facdee5bfd935d9cb031a1698a5
|
||||
hop_ephemeral_pubkey[2] = 0x03bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0
|
||||
|
||||
hop_shared_secret[3] = 0x21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d
|
||||
hop_blinding_factor[3] = 0x7cfe0b699f35525029ae0fa437c69d0f20f7ed4e3916133f9cacbb13c82ff262
|
||||
hop_ephemeral_pubkey[3] = 0x031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd43595
|
||||
|
||||
hop_shared_secret[4] = 0xb5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328
|
||||
hop_blinding_factor[4] = 0xc96e00dddaf57e7edcd4fb5954be5b65b09f17cb6d20651b4e90315be5779205
|
||||
hop_ephemeral_pubkey[4] = 0x03a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f4
|
||||
*/
|
||||
test("generate ephemereal keys and secrets") {
|
||||
val (ephkeys, sharedsecrets) = computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys)
|
||||
assert(ephkeys(0) == PublicKey(BinaryData("0x02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")))
|
||||
assert(sharedsecrets(0) == BinaryData("0x53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66"))
|
||||
assert(ephkeys(1) == PublicKey(BinaryData("0x028f9438bfbf7feac2e108d677e3a82da596be706cc1cf342b75c7b7e22bf4e6e2")))
|
||||
assert(sharedsecrets(1) == BinaryData("0xa6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae"))
|
||||
assert(ephkeys(2) == PublicKey(BinaryData("0x03bfd8225241ea71cd0843db7709f4c222f62ff2d4516fd38b39914ab6b83e0da0")))
|
||||
assert(sharedsecrets(2) == BinaryData("0x3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc"))
|
||||
assert(ephkeys(3) == PublicKey(BinaryData("0x031dde6926381289671300239ea8e57ffaf9bebd05b9a5b95beaf07af05cd43595")))
|
||||
assert(sharedsecrets(3) == BinaryData("0x21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d"))
|
||||
assert(ephkeys(4) == PublicKey(BinaryData("0x03a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f4")))
|
||||
assert(sharedsecrets(4) == BinaryData("0xb5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328"))
|
||||
}
|
||||
|
||||
/*
|
||||
filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac
|
||||
*/
|
||||
test("generate filler") {
|
||||
val (_, sharedsecrets) = computeEphemerealPublicKeysAndSharedSecrets(sessionKey, publicKeys)
|
||||
val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, 20)
|
||||
assert(filler == BinaryData("0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac"))
|
||||
}
|
||||
|
||||
test("create packet (reference test vector)") {
|
||||
val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, payloads, associatedData)
|
||||
assert(onion.serialize == BinaryData("0x0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a716a996c7845c93d90e4ecbb9bde4ece2f69425c99e4bc820e44485455f135edc0d10f7d61ab590531cf08000179a333a347f8b4072f216400406bdf3bf038659793d4a1fd7b246979e3150a0a4cb052c9ec69acf0f48c3d39cd55675fe717cb7d80ce721caad69320c3a469a202f1e468c67eaf7a7cd8226d0fd32f7b48084dca885d56047694762b67021713ca673929c163ec36e04e40ca8e1c6d17569419d3039d9a1ec866abe044a9ad635778b961fc0776dc832b3a451bd5d35072d2269cf9b040f6b7a7dad84fb114ed413b1426cb96ceaf83825665ed5a1d002c1687f92465b49ed4c7f0218ff8c6c7dd7221d589c65b3b9aaa71a41484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172307c7268724c3618e6817abd793adc214a0dc0bc616816632f27ea336fb56dfd"))
|
||||
|
||||
val Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize)
|
||||
val Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize)
|
||||
val Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize)
|
||||
val Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize)
|
||||
val Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize)
|
||||
assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads)
|
||||
|
||||
val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4)
|
||||
assert(packets(0).hmac == BinaryData("0x2bdc5227c8eb8ba5fcfc15cfc2aa578ff208c106646d0652cd289c0a37e445bb"))
|
||||
assert(packets(1).hmac == BinaryData("0x28430b210c0af631ef80dc8594c08557ce4626bdd3593314624a588cc083a1d9"))
|
||||
assert(packets(2).hmac == BinaryData("0x4e888d0cc6a90e7f857af18ac858834ac251d0d1c196d198df48a0c5bf816803"))
|
||||
assert(packets(3).hmac == BinaryData("0x42c10947e06bda75b35ac2a9e38005479a6feac51468712e751c71a1dcf3e31b"))
|
||||
// this means that node #4 us the last node
|
||||
assert(packets(4).hmac == BinaryData("0x0000000000000000000000000000000000000000000000000000000000000000"))
|
||||
}
|
||||
|
||||
test("last node replies with an error message") {
|
||||
// route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4
|
||||
|
||||
// origin build the onion packet
|
||||
val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData)
|
||||
|
||||
// each node parses and forwards the packet
|
||||
// node #0
|
||||
val ParsedPacket(payload0, packet1, sharedSecret0) = parsePacket(privKeys(0), associatedData, packet.serialize)
|
||||
// node #1
|
||||
val ParsedPacket(payload1, packet2, sharedSecret1) = parsePacket(privKeys(1), associatedData, packet1.serialize)
|
||||
// node #2
|
||||
val ParsedPacket(payload2, packet3, sharedSecret2) = parsePacket(privKeys(2), associatedData, packet2.serialize)
|
||||
// node #3
|
||||
val ParsedPacket(payload3, packet4, sharedSecret3) = parsePacket(privKeys(3), associatedData, packet3.serialize)
|
||||
// node #4
|
||||
val ParsedPacket(payload4, packet5, sharedSecret4) = parsePacket(privKeys(4), associatedData, packet4.serialize)
|
||||
assert(packet5.isLastPacket)
|
||||
|
||||
// node #4 want to reply with an error message
|
||||
val error = createErrorPacket(sharedSecret4, TemporaryNodeFailure)
|
||||
assert(error == BinaryData("a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4"))
|
||||
// assert(error == BinaryData("69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2"))
|
||||
// error sent back to 3, 2, 1 and 0
|
||||
val error1 = forwardErrorPacket(error, sharedSecret3)
|
||||
assert(error1 == BinaryData("c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270"))
|
||||
// assert(error1 == BinaryData("08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93"))
|
||||
|
||||
val error2 = forwardErrorPacket(error1, sharedSecret2)
|
||||
assert(error2 == BinaryData("a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3"))
|
||||
// assert(error2 == BinaryData("6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd"))
|
||||
|
||||
val error3 = forwardErrorPacket(error2, sharedSecret1)
|
||||
assert(error3 == BinaryData("aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921"))
|
||||
// assert(error3 == BinaryData("669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8"))
|
||||
|
||||
val error4 = forwardErrorPacket(error3, sharedSecret0)
|
||||
assert(error4 == BinaryData("9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d"))
|
||||
// assert(error4 == BinaryData("500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366"))
|
||||
|
||||
|
||||
// origin parses error packet and can see that it comes from node #4
|
||||
val Some(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets)
|
||||
assert(pubkey == publicKeys(4))
|
||||
assert(failure == TemporaryNodeFailure)
|
||||
}
|
||||
|
||||
test("intermediate node replies with an error message") {
|
||||
// route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4
|
||||
|
||||
// origin build the onion packet
|
||||
val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData)
|
||||
|
||||
// each node parses and forwards the packet
|
||||
// node #0
|
||||
val ParsedPacket(payload0, packet1, sharedSecret0) = parsePacket(privKeys(0), associatedData, packet.serialize)
|
||||
// node #1
|
||||
val ParsedPacket(payload1, packet2, sharedSecret1) = parsePacket(privKeys(1), associatedData, packet1.serialize)
|
||||
// node #2
|
||||
val ParsedPacket(payload2, packet3, sharedSecret2) = parsePacket(privKeys(2), associatedData, packet2.serialize)
|
||||
|
||||
// node #2 want to reply with an error message
|
||||
val error = createErrorPacket(sharedSecret2, InvalidRealm)
|
||||
|
||||
// error sent back to 1 and 0
|
||||
val error1 = forwardErrorPacket(error, sharedSecret1)
|
||||
val error2 = forwardErrorPacket(error1, sharedSecret0)
|
||||
|
||||
// origin parses error packet and can see that it comes from node #2
|
||||
val Some(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets)
|
||||
assert(pubkey == publicKeys(2))
|
||||
assert(failure == InvalidRealm)
|
||||
}
|
||||
}
|
||||
|
||||
object SphinxSpec {
|
||||
val privKeys = Seq(
|
||||
PrivateKey(BinaryData("0x4141414141414141414141414141414141414141414141414141414141414141"), compressed = true),
|
||||
PrivateKey(BinaryData("0x4242424242424242424242424242424242424242424242424242424242424242"), compressed = true),
|
||||
PrivateKey(BinaryData("0x4343434343434343434343434343434343434343434343434343434343434343"), compressed = true),
|
||||
PrivateKey(BinaryData("0x4444444444444444444444444444444444444444444444444444444444444444"), compressed = true),
|
||||
PrivateKey(BinaryData("0x4545454545454545454545454545454545454545454545454545454545454545"), compressed = true)
|
||||
)
|
||||
val publicKeys = privKeys.map(_.publicKey)
|
||||
assert(publicKeys == Seq(
|
||||
PublicKey(BinaryData("0x02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")),
|
||||
PublicKey(BinaryData("0x0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")),
|
||||
PublicKey(BinaryData("0x027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")),
|
||||
PublicKey(BinaryData("0x032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")),
|
||||
PublicKey(BinaryData("0x02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"))
|
||||
))
|
||||
|
||||
val sessionKey: PrivateKey = PrivateKey(BinaryData("0x4141414141414141414141414141414141414141414141414141414141414141"), compressed = true)
|
||||
val payloads = Seq(
|
||||
BinaryData("0x000000000000000000000000000000000000000000000000000000000000000000"),
|
||||
BinaryData("0x000101010101010101000000010000000100000000000000000000000000000000"),
|
||||
BinaryData("0x000202020202020202000000020000000200000000000000000000000000000000"),
|
||||
BinaryData("0x000303030303030303000000030000000300000000000000000000000000000000"),
|
||||
BinaryData("0x000404040404040404000000040000000400000000000000000000000000000000"))
|
||||
|
||||
val associatedData: BinaryData = "0x4242424242424242424242424242424242424242424242424242424242424242"
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.sql.DriverManager
|
||||
|
||||
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePreimagesDb}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SqliteChannelsDbSpec extends FunSuite {
|
||||
|
||||
def inmem = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
test("init sqlite 2 times in a row") {
|
||||
val sqlite = inmem
|
||||
val db1 = new SqliteChannelsDb(sqlite)
|
||||
val db2 = new SqliteChannelsDb(sqlite)
|
||||
}
|
||||
|
||||
test("add/remove/list channels") {
|
||||
val sqlite = inmem
|
||||
val db = new SqliteChannelsDb(sqlite)
|
||||
new SqlitePreimagesDb(sqlite) // needed by db.removeChannel
|
||||
|
||||
val channel = ChannelStateSpec.normal
|
||||
|
||||
assert(db.listChannels().toSet === Set.empty)
|
||||
db.addOrUpdateChannel(channel)
|
||||
db.addOrUpdateChannel(channel)
|
||||
assert(db.listChannels() === List(channel))
|
||||
db.removeChannel(channel.channelId)
|
||||
assert(db.listChannels() === Nil)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
import java.sql.DriverManager
|
||||
|
||||
import fr.acinq.bitcoin.{Block, Crypto}
|
||||
import fr.acinq.eclair.db.sqlite.SqliteNetworkDb
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.sqlite.SQLiteException
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SqliteNetworkDbSpec extends FunSuite {
|
||||
|
||||
def inmem = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
test("init sqlite 2 times in a row") {
|
||||
val sqlite = inmem
|
||||
val db1 = new SqliteNetworkDb(sqlite)
|
||||
val db2 = new SqliteNetworkDb(sqlite)
|
||||
}
|
||||
|
||||
test("add/remove/list nodes") {
|
||||
val sqlite = inmem
|
||||
val db = new SqliteNetworkDb(sqlite)
|
||||
|
||||
val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", (100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil)
|
||||
|
||||
assert(db.listNodes().toSet === Set.empty)
|
||||
db.addNode(node_1)
|
||||
db.addNode(node_1) // duplicate is ignored
|
||||
assert(db.listNodes().size === 1)
|
||||
db.addNode(node_2)
|
||||
db.addNode(node_3)
|
||||
assert(db.listNodes().toSet === Set(node_1, node_2, node_3))
|
||||
db.removeNode(node_2.nodeId)
|
||||
assert(db.listNodes().toSet === Set(node_1, node_3))
|
||||
db.updateNode(node_1)
|
||||
}
|
||||
|
||||
test("add/remove/list channels and channel_updates") {
|
||||
val sqlite = inmem
|
||||
val db = new SqliteNetworkDb(sqlite)
|
||||
|
||||
def sig = Crypto.encodeSignature(Crypto.sign(randomKey.toBin, randomKey)) :+ 1.toByte
|
||||
|
||||
val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, 42, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig)
|
||||
val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, 43, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig)
|
||||
val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, 44, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig)
|
||||
|
||||
assert(db.listChannels().toSet === Set.empty)
|
||||
db.addChannel(channel_1)
|
||||
db.addChannel(channel_1) // duplicate is ignored
|
||||
assert(db.listChannels().size === 1)
|
||||
db.addChannel(channel_2)
|
||||
db.addChannel(channel_3)
|
||||
assert(db.listChannels().toSet === Set(channel_1, channel_2, channel_3))
|
||||
db.removeChannel(channel_2.shortChannelId)
|
||||
assert(db.listChannels().toSet === Set(channel_1, channel_3))
|
||||
|
||||
val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, 42, 5, 7000000, 50000, 100, true)
|
||||
val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, 43, 5, 7000000, 50000, 100, true)
|
||||
val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey, randomKey.publicKey, 44, 5, 7000000, 50000, 100, true)
|
||||
|
||||
assert(db.listChannelUpdates().toSet === Set.empty)
|
||||
db.addChannelUpdate(channel_update_1)
|
||||
db.addChannelUpdate(channel_update_1) // duplicate is ignored
|
||||
assert(db.listChannelUpdates().size === 1)
|
||||
intercept[SQLiteException](db.addChannelUpdate(channel_update_2))
|
||||
db.addChannelUpdate(channel_update_3)
|
||||
db.removeChannel(channel_3.shortChannelId)
|
||||
assert(db.listChannels().toSet === Set(channel_1))
|
||||
assert(db.listChannelUpdates().toSet === Set(channel_update_1))
|
||||
db.updateChannelUpdate(channel_update_1)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
import java.sql.DriverManager
|
||||
|
||||
import fr.acinq.eclair.db.sqlite.SqlitePeersDb
|
||||
import fr.acinq.eclair.randomKey
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SqlitePeersDbSpec extends FunSuite {
|
||||
|
||||
def inmem = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
test("init sqlite 2 times in a row") {
|
||||
val sqlite = inmem
|
||||
val db1 = new SqlitePeersDb(sqlite)
|
||||
val db2 = new SqlitePeersDb(sqlite)
|
||||
}
|
||||
|
||||
test("add/remove/list peers") {
|
||||
val sqlite = inmem
|
||||
val db = new SqlitePeersDb(sqlite)
|
||||
|
||||
val peer_1 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 1111))
|
||||
val peer_1_bis = (peer_1._1, new InetSocketAddress(InetAddress.getLoopbackAddress, 1112))
|
||||
val peer_2 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 2222))
|
||||
val peer_3 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 3333))
|
||||
|
||||
assert(db.listPeers().toSet === Set.empty)
|
||||
db.addOrUpdatePeer(peer_1._1, peer_1._2)
|
||||
db.addOrUpdatePeer(peer_1._1, peer_1._2) // duplicate is ignored
|
||||
assert(db.listPeers().size === 1)
|
||||
db.addOrUpdatePeer(peer_2._1, peer_2._2)
|
||||
db.addOrUpdatePeer(peer_3._1, peer_3._2)
|
||||
assert(db.listPeers().toSet === Set(peer_1, peer_2, peer_3))
|
||||
db.removePeer(peer_2._1)
|
||||
assert(db.listPeers().toSet === Set(peer_1, peer_3))
|
||||
db.addOrUpdatePeer(peer_1_bis._1, peer_1_bis._2)
|
||||
assert(db.listPeers().toSet === Set(peer_1_bis, peer_3))
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package fr.acinq.eclair.db
|
||||
|
||||
import java.sql.DriverManager
|
||||
|
||||
import fr.acinq.eclair.db.sqlite.SqlitePreimagesDb
|
||||
import fr.acinq.eclair.randomBytes
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SqlitePreimagesDbSpec extends FunSuite {
|
||||
|
||||
def inmem = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
test("init sqlite 2 times in a row") {
|
||||
val sqlite = inmem
|
||||
val db1 = new SqlitePreimagesDb(sqlite)
|
||||
val db2 = new SqlitePreimagesDb(sqlite)
|
||||
}
|
||||
|
||||
test("add/remove/list preimages") {
|
||||
val sqlite = inmem
|
||||
val db = new SqlitePreimagesDb(sqlite)
|
||||
|
||||
val channelId = randomBytes(32)
|
||||
val preimage0 = randomBytes(32)
|
||||
val preimage1 = randomBytes(32)
|
||||
val preimage2 = randomBytes(32)
|
||||
val preimage3 = randomBytes(32)
|
||||
|
||||
assert(db.listPreimages(channelId).toSet === Set.empty)
|
||||
db.addPreimage(channelId, 0, preimage0)
|
||||
db.addPreimage(channelId, 0, preimage0) // duplicate
|
||||
db.addPreimage(channelId, 1, preimage1)
|
||||
db.addPreimage(channelId, 2, preimage2)
|
||||
assert(db.listPreimages(channelId).sortBy(_._2) === (channelId, 0, preimage0) :: (channelId, 1, preimage1) :: (channelId, 2, preimage2) :: Nil)
|
||||
db.removePreimage(channelId, 1)
|
||||
assert(db.listPreimages(channelId).sortBy(_._2) === (channelId, 0, preimage0) :: (channelId, 2, preimage2) :: Nil)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,653 +0,0 @@
|
||||
package fr.acinq.eclair.integration
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.nio.file.Files
|
||||
import java.util.{Properties, UUID}
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjWallet
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.io.Disconnect
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import fr.acinq.eclair.payment.{State => _, _}
|
||||
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Bitcoinj, Globals, Kit, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.Transaction
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
/**
|
||||
* Created by PM on 15/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
@Ignore
|
||||
class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
System.setProperty("spvtest", "true")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BasicIntegrationSpvSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2, param3) => bitcoinrpcclient.invoke(method, param1, param2, param3) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
setup.system.terminate()
|
||||
}
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
|
||||
def instantiateEclairNode(name: String, config: Config) = {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name")
|
||||
datadir.mkdirs()
|
||||
new PrintWriter(new File(datadir, "eclair.conf")) {
|
||||
write(config.root().render());
|
||||
close
|
||||
}
|
||||
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
|
||||
val kit = Await.result(setup.bootstrap, 10 seconds)
|
||||
setup.bitcoin.asInstanceOf[Bitcoinj].bitcoinjKit.awaitRunning()
|
||||
nodes = nodes + (name -> kit)
|
||||
}
|
||||
|
||||
def javaProps(props: Seq[(String, String)]) = {
|
||||
val properties = new Properties()
|
||||
props.foreach(p => properties.setProperty(p._1, p._2))
|
||||
properties
|
||||
}
|
||||
|
||||
test("starting eclair nodes") {
|
||||
import collection.JavaConversions._
|
||||
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> true, "eclair.chain" -> "regtest", "eclair.bitcoinj.static-peers.0.host" -> "localhost", "eclair.bitcoinj.static-peers.0.port" -> 28333, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false, "eclair.delay-blocks" -> 6))
|
||||
//instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29730, "eclair.api.port" -> 28080)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) // NB: eclair.payment-handler = noop allows us to manually fulfill htlcs
|
||||
//instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
}
|
||||
|
||||
def sendFunds(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
val address = Await.result(node.wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond({
|
||||
node.wallet.getBalance.pipeTo(sender.ref)
|
||||
sender.expectMsgType[Satoshi] > Satoshi(0)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("fund eclair wallets") {
|
||||
//sendFunds(nodes("A"))
|
||||
//sendFunds(nodes("B"))
|
||||
sendFunds(nodes("C"))
|
||||
//sendFunds(nodes("D"))
|
||||
//sendFunds(nodes("E"))
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
val eventListener1 = TestProbe()
|
||||
val eventListener2 = TestProbe()
|
||||
node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged])
|
||||
node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged])
|
||||
val sender = TestProbe()
|
||||
sender.send(node1.switchboard, NewConnection(
|
||||
remoteNodeId = node2.nodeParams.privateKey.publicKey,
|
||||
address = node2.nodeParams.publicAddresses.head,
|
||||
newChannel_opt = Some(NewChannel(Satoshi(fundingSatoshis), MilliSatoshi(pushMsat), None))))
|
||||
sender.expectMsgAnyOf(10 seconds, "connected", s"already connected to nodeId=${node2.nodeParams.privateKey.publicKey.toBin}")
|
||||
awaitCond(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
awaitCond(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
}
|
||||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4}
|
||||
//
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
|
||||
//connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
//connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
//connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
//connect(nodes("B"), nodes("E"), 5000000, 0)
|
||||
//connect(nodes("E"), nodes("C"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F1"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F2"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F3"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
|
||||
// a channel has two endpoints
|
||||
val channelEndpointsCount = nodes.values.foldLeft(0) {
|
||||
case (sum, setup) =>
|
||||
sender.send(setup.register, 'channels)
|
||||
val channels = sender.expectMsgType[Map[BinaryData, ActorRef]]
|
||||
sum + channels.size
|
||||
}
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
// confirming the funding tx
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(60 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == NORMAL) count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int) = {
|
||||
val sender = TestProbe()
|
||||
subset.foreach {
|
||||
case (_, setup) =>
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'nodes)
|
||||
sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].size == updates
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
|
||||
test("wait for network announcements") {
|
||||
val sender = TestProbe()
|
||||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 4))
|
||||
sender.expectMsgType[JValue]
|
||||
awaitAnnouncements(nodes, 3, 2, 4)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D") {
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey))
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an invalid expiry delta for C") {
|
||||
val sender = TestProbe()
|
||||
// to simulate this, we will update C's relay params
|
||||
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel
|
||||
sender.send(nodes("D").register, 'shortIds)
|
||||
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head
|
||||
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.blockId, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will receive an error from C that include the updated channel update, then will retry the payment
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
// in the meantime, the router will have updated its state
|
||||
awaitCond({
|
||||
sender.send(nodes("A").router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD)
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// finally we retry the same payment, this time successfully
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an amount greater than capacity of C-D") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an unknown payment hash") {
|
||||
val sender = TestProbe()
|
||||
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, pr)
|
||||
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, UnknownPaymentHash))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a lower amount than requested") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with too much overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a reasonable overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently use p2pkh script Helpers.getFinalScriptPubKey
|
||||
*
|
||||
* @param scriptPubKey
|
||||
* @return
|
||||
*/
|
||||
def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match {
|
||||
case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)
|
||||
case OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUAL :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, pubKeyHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
def incomingTxes(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
(for {
|
||||
w <- nodes("F1").wallet.asInstanceOf[BitcoinjWallet].fWallet
|
||||
txes = w.getWalletTransactions
|
||||
incomingTxes = txes.toSet.filter(tx => tx.getTransaction.getValueSentToMe(w).longValue() > 0)
|
||||
} yield incomingTxes).pipeTo(sender.ref)
|
||||
sender.expectMsgType[Set[Transaction]]
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
val initialTxesC = incomingTxes(nodes("C"))
|
||||
val initialTxesF1 = incomingTxes(nodes("F1"))
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F1").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F1").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then have C unilateral close the channel (which will make F redeem the htlc onchain)
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then wait for F to detect the unilateral close and go to CLOSING state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == CLOSING
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then fulfill the htlc, which will make F redeem it on-chain
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have received the on-chain tx corresponding to the redeemed htlc
|
||||
awaitCond({
|
||||
incomingTxes(nodes("F1")).size - initialTxesF1.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we then generate enough blocks so that C gets its main delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// and C will have its main output
|
||||
awaitCond({
|
||||
incomingTxes(nodes("C")).size - initialTxesC.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F2").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F2").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// then we have F unilateral close the channel
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
// we then generate enough blocks so that F gets its htlc-success delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc and C will have its main output
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 1 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F3").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F3").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F4").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// then we ask F to unilaterally close the channel
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
|
||||
}
|
||||
|
||||
ignore("generate and validate lots of channels") {
|
||||
implicit val extendedClient = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
// we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random
|
||||
logger.info(s"generating fake channels")
|
||||
val sender = TestProbe()
|
||||
val channels = for (i <- 0 until 242) yield {
|
||||
// let's generate a block every 10 txs so that we can compute short ids
|
||||
if (i % 10 == 0) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
AnnouncementsBatchValidationSpec.simulateChannel
|
||||
}
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
logger.info(s"simulated ${channels.size} channels")
|
||||
// then we make the announcements
|
||||
val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c))
|
||||
announcements.foreach(ann => nodes("A").router ! ann)
|
||||
awaitCond({
|
||||
sender.send(nodes("D").router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared
|
||||
}, max = 120 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,683 +0,0 @@
|
||||
package fr.acinq.eclair.integration
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.nio.file.Files
|
||||
import java.util.{Properties, UUID}
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.io.Disconnect
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import fr.acinq.eclair.payment.{State => _, _}
|
||||
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, Kit, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
/**
|
||||
* Created by PM on 15/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
setup.system.terminate()
|
||||
}
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
|
||||
def instantiateEclairNode(name: String, config: Config) = {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name")
|
||||
datadir.mkdirs()
|
||||
new PrintWriter(new File(datadir, "eclair.conf")) {
|
||||
write(config.root().render());
|
||||
close
|
||||
}
|
||||
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
|
||||
val kit = Await.result(setup.bootstrap, 10 seconds)
|
||||
nodes = nodes + (name -> kit)
|
||||
}
|
||||
|
||||
def javaProps(props: Seq[(String, String)]) = {
|
||||
val properties = new Properties()
|
||||
props.foreach(p => properties.setProperty(p._1, p._2))
|
||||
properties
|
||||
}
|
||||
|
||||
test("starting eclair nodes") {
|
||||
import collection.JavaConversions._
|
||||
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> false, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false))
|
||||
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29730, "eclair.api.port" -> 28080)).withFallback(commonConfig))
|
||||
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
|
||||
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
|
||||
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
|
||||
instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) // NB: eclair.payment-handler = noop allows us to manually fulfill htlcs
|
||||
instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.server.port" -> 29739, "eclair.api.port" -> 28089)).withFallback(commonConfig))
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
val eventListener1 = TestProbe()
|
||||
val eventListener2 = TestProbe()
|
||||
node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged])
|
||||
node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged])
|
||||
val sender = TestProbe()
|
||||
sender.send(node1.switchboard, NewConnection(
|
||||
remoteNodeId = node2.nodeParams.privateKey.publicKey,
|
||||
address = node2.nodeParams.publicAddresses.head,
|
||||
newChannel_opt = Some(NewChannel(Satoshi(fundingSatoshis), MilliSatoshi(pushMsat), None))))
|
||||
sender.expectMsgAnyOf(10 seconds, "connected", s"already connected to nodeId=${node2.nodeParams.privateKey.publicKey.toBin}")
|
||||
// funder transitions
|
||||
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL)
|
||||
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL)
|
||||
// fundee transitions
|
||||
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL)
|
||||
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED)
|
||||
}
|
||||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4}
|
||||
//
|
||||
|
||||
connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
connect(nodes("B"), nodes("E"), 5000000, 0)
|
||||
connect(nodes("E"), nodes("C"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F1"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F2"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F3"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F5"), 5000000, 0)
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
|
||||
// a channel has two endpoints
|
||||
val channelEndpointsCount = nodes.values.foldLeft(0) {
|
||||
case (sum, setup) =>
|
||||
sender.send(setup.register, 'channels)
|
||||
val channels = sender.expectMsgType[Map[BinaryData, ActorRef]]
|
||||
sum + channels.size
|
||||
}
|
||||
|
||||
// each funder sets up a WatchConfirmed on the parent tx, we need to make sure it has been received by the watcher
|
||||
var watches1 = Set.empty[Watch]
|
||||
awaitCond({
|
||||
watches1 = nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}
|
||||
watches1.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount / 2
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
// confirming the parent tx of the funding
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(30 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED) count = count + 1
|
||||
}
|
||||
}
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
val watches2 = nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}
|
||||
(watches2 -- watches1).count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
|
||||
// confirming the funding tx
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(60 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](30 seconds).currentState == NORMAL) count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int) = {
|
||||
val sender = TestProbe()
|
||||
subset.foreach {
|
||||
case (_, setup) =>
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'nodes)
|
||||
sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].size == updates
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
|
||||
test("wait for network announcements") {
|
||||
val sender = TestProbe()
|
||||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 4))
|
||||
sender.expectMsgType[JValue]
|
||||
awaitAnnouncements(nodes, 10, 10, 20)
|
||||
}
|
||||
|
||||
test("send an HTLC A->D") {
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey))
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with an invalid expiry delta for C") {
|
||||
val sender = TestProbe()
|
||||
// to simulate this, we will update C's relay params
|
||||
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel
|
||||
sender.send(nodes("D").register, 'shortIds)
|
||||
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head
|
||||
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will receive an error from C that include the updated channel update, then will retry the payment
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
// in the meantime, the router will have updated its state
|
||||
awaitCond({
|
||||
sender.send(nodes("A").router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD)
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// finally we retry the same payment, this time successfully
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with an amount greater than capacity of C-D") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with an unknown payment hash") {
|
||||
val sender = TestProbe()
|
||||
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, pr)
|
||||
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, UnknownPaymentHash))
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with a lower amount than requested") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with too much overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
test("send an HTLC A->D with a reasonable overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently use p2pkh script Helpers.getFinalScriptPubKey
|
||||
*
|
||||
* @param scriptPubKey
|
||||
* @return
|
||||
*/
|
||||
def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match {
|
||||
case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F1").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressF = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F1").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then have C unilateral close the channel (which will make F redeem the htlc onchain)
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then wait for F to detect the unilateral close and go to CLOSING state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == CLOSING
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then fulfill the htlc, which will make F redeem it on-chain
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we then generate enough blocks so that C gets its main delayed output
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 145))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and C will have its main output
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 9, 18)
|
||||
}
|
||||
|
||||
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F2").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressF = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F2").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// then we have F unilateral close the channel
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
// we then generate enough blocks so that F gets its htlc-success delayed output
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 145))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// and C will have its main output
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F3").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 11))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// this will fail the htlc
|
||||
val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
assert(failed.paymentHash === paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 150))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F4").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// then we ask F to unilaterally close the channel
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 11))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// this will fail the htlc
|
||||
val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
assert(failed.paymentHash === paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 145))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
|
||||
}
|
||||
|
||||
test("punish a node that has published a revoked commit tx") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// first we send 3 mBTC to F so that it has a balance
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
// then we find the id of F's only channel
|
||||
sender.send(nodes("F5").register, 'channels)
|
||||
val channelId = sender.expectMsgType[Map[BinaryData, ActorRef]].head._1
|
||||
// we then wait for F to have a main output
|
||||
awaitCond({
|
||||
sender.send(nodes("F5").register, Forward(channelId, CMD_GETSTATEDATA))
|
||||
sender.expectMsgType[DATA_NORMAL].commitments.localCommit.index == 2
|
||||
}, max = 5 seconds)
|
||||
// and we use it to get its current commitment tx
|
||||
sender.send(nodes("F5").register, Forward(channelId, CMD_GETSTATEDATA))
|
||||
val localCommitTxF = sender.expectMsgType[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
// we now send some more money to F so that it creates a new commitment tx
|
||||
val amountMsat1 = MilliSatoshi(100000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat1, "1 coffee"))
|
||||
val pr1 = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq1 = SendPayment(100000000L, pr1.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq1)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
// we also retrieve C's default final address
|
||||
sender.send(nodes("C").register, Forward(channelId, CMD_GETSTATEDATA))
|
||||
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
|
||||
// and we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// then we publish F's previous commit tx
|
||||
sender.send(bitcoincli, BitcoinReq("sendrawtransaction", Transaction.write(localCommitTxF.commitTx.tx).toString()))
|
||||
sender.expectMsgType[JValue](10000 seconds)
|
||||
// at this point C should have 2 recv transactions: its previous main output and the one it took from F as a punishment
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// this will remove the channel
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
|
||||
}
|
||||
|
||||
test("generate and validate lots of channels") {
|
||||
implicit val extendedClient = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
// we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random
|
||||
logger.info(s"generating fake channels")
|
||||
val sender = TestProbe()
|
||||
val channels = for (i <- 0 until 242) yield {
|
||||
// let's generate a block every 10 txs so that we can compute short ids
|
||||
if (i % 10 == 0) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
AnnouncementsBatchValidationSpec.simulateChannel
|
||||
}
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
logger.info(s"simulated ${channels.size} channels")
|
||||
// then we make the announcements
|
||||
val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c))
|
||||
announcements.foreach(ann => nodes("A").router ! ann)
|
||||
awaitCond({
|
||||
sender.send(nodes("D").router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared
|
||||
}, max = 120 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,170 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket}
|
||||
import fr.acinq.eclair.payment.PaymentHop.nodeFee
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle._
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import scodec.bits.BitVector
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class HtlcGenerationSpec extends FunSuite {
|
||||
|
||||
test("compute fees") {
|
||||
val feeBaseMsat = 150000L
|
||||
val feeProportionalMillionth = 4L
|
||||
val htlcAmountMsat = 42000000
|
||||
// spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000
|
||||
val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000
|
||||
val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat)
|
||||
assert(ref === fee)
|
||||
}
|
||||
|
||||
import HtlcGenerationSpec._
|
||||
|
||||
test("compute payloads with fees and expiry delta") {
|
||||
|
||||
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
|
||||
|
||||
assert(firstAmountMsat === amount_ab)
|
||||
assert(firstExpiry === expiry_ab)
|
||||
assert(payloads ===
|
||||
PerHopPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc) ::
|
||||
PerHopPayload(channelUpdate_cd.shortChannelId, amount_cd, expiry_cd) ::
|
||||
PerHopPayload(channelUpdate_de.shortChannelId, amount_de, expiry_de) ::
|
||||
PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)
|
||||
}
|
||||
|
||||
test("build onion") {
|
||||
|
||||
val (_, _, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))
|
||||
val nodes = hops.map(_.nextNodeId)
|
||||
val PacketAndSecrets(packet_b, _) = buildOnion(nodes, payloads, paymentHash)
|
||||
assert(packet_b.serialize.size === Sphinx.PacketLength)
|
||||
|
||||
// let's peel the onion
|
||||
val ParsedPacket(bin_b, packet_c, _) = Sphinx.parsePacket(priv_b, paymentHash, packet_b.serialize)
|
||||
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
|
||||
assert(packet_c.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_b.amtToForward === amount_bc)
|
||||
assert(payload_b.outgoingCltvValue === expiry_bc)
|
||||
|
||||
val ParsedPacket(bin_c, packet_d, _) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize)
|
||||
val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value
|
||||
assert(packet_d.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_c.amtToForward === amount_cd)
|
||||
assert(payload_c.outgoingCltvValue === expiry_cd)
|
||||
|
||||
val ParsedPacket(bin_d, packet_e, _) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize)
|
||||
val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value
|
||||
assert(packet_e.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_d.amtToForward === amount_de)
|
||||
assert(payload_d.outgoingCltvValue === expiry_de)
|
||||
|
||||
val ParsedPacket(bin_e, packet_random, _) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize)
|
||||
val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value
|
||||
assert(packet_random.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_e.amtToForward === finalAmountMsat)
|
||||
assert(payload_e.outgoingCltvValue === finalExpiry)
|
||||
}
|
||||
|
||||
test("build a command including the onion") {
|
||||
|
||||
val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
|
||||
assert(add.amountMsat > finalAmountMsat)
|
||||
assert(add.expiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta)
|
||||
assert(add.paymentHash === paymentHash)
|
||||
assert(add.onion.length === Sphinx.PacketLength)
|
||||
|
||||
// let's peel the onion
|
||||
val ParsedPacket(bin_b, packet_c, _) = Sphinx.parsePacket(priv_b, paymentHash, add.onion)
|
||||
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
|
||||
assert(packet_c.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_b.amtToForward === amount_bc)
|
||||
assert(payload_b.outgoingCltvValue === expiry_bc)
|
||||
|
||||
val ParsedPacket(bin_c, packet_d, _) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize)
|
||||
val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value
|
||||
assert(packet_d.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_c.amtToForward === amount_cd)
|
||||
assert(payload_c.outgoingCltvValue === expiry_cd)
|
||||
|
||||
val ParsedPacket(bin_d, packet_e, _) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize)
|
||||
val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value
|
||||
assert(packet_e.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_d.amtToForward === amount_de)
|
||||
assert(payload_d.outgoingCltvValue === expiry_de)
|
||||
|
||||
val ParsedPacket(bin_e, packet_random, _) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize)
|
||||
val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value
|
||||
assert(packet_random.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_e.amtToForward === finalAmountMsat)
|
||||
assert(payload_e.outgoingCltvValue === finalExpiry)
|
||||
}
|
||||
|
||||
test("build a command with no hops") {
|
||||
val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops.take(1))
|
||||
|
||||
assert(add.amountMsat === finalAmountMsat)
|
||||
assert(add.expiry === finalExpiry)
|
||||
assert(add.paymentHash === paymentHash)
|
||||
assert(add.onion.size === Sphinx.PacketLength)
|
||||
|
||||
// let's peel the onion
|
||||
val ParsedPacket(bin_b, packet_random, _) = Sphinx.parsePacket(priv_b, paymentHash, add.onion)
|
||||
val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value
|
||||
assert(packet_random.serialize.size === Sphinx.PacketLength)
|
||||
assert(payload_b.amtToForward === finalAmountMsat)
|
||||
assert(payload_b.outgoingCltvValue === finalExpiry)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object HtlcGenerationSpec {
|
||||
val (priv_a, priv_b, priv_c, priv_d, priv_e) = (randomKey, randomKey, randomKey, randomKey, randomKey)
|
||||
val (a, b, c, d, e) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey)
|
||||
val sig = Crypto.encodeSignature(Crypto.sign(Crypto.sha256(BinaryData.empty), priv_a)) :+ 1.toByte
|
||||
val defaultChannelUpdate = ChannelUpdate(sig, Block.RegtestGenesisBlock.hash, 0, 0, "0000", 0, 42000, 0, 0)
|
||||
val channelUpdate_ab = defaultChannelUpdate.copy(shortChannelId = 1, cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7)
|
||||
val channelUpdate_bc = defaultChannelUpdate.copy(shortChannelId = 2, cltvExpiryDelta = 5, feeBaseMsat = 153000, feeProportionalMillionths = 4)
|
||||
val channelUpdate_cd = defaultChannelUpdate.copy(shortChannelId = 3, cltvExpiryDelta = 10, feeBaseMsat = 60000, feeProportionalMillionths = 1)
|
||||
val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = 4, cltvExpiryDelta = 7, feeBaseMsat = 766000, feeProportionalMillionths = 10)
|
||||
|
||||
// simple route a -> b -> c -> d -> e
|
||||
|
||||
val hops =
|
||||
Hop(a, b, channelUpdate_ab) ::
|
||||
Hop(b, c, channelUpdate_bc) ::
|
||||
Hop(c, d, channelUpdate_cd) ::
|
||||
Hop(d, e, channelUpdate_de) :: Nil
|
||||
|
||||
val finalAmountMsat = 42000000L
|
||||
val currentBlockCount = 420000
|
||||
val finalExpiry = currentBlockCount + defaultMinFinalCltvExpiry
|
||||
val paymentPreimage = BinaryData("42" * 32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
|
||||
val expiry_de = currentBlockCount + defaultMinFinalCltvExpiry
|
||||
val amount_de = finalAmountMsat
|
||||
val fee_d = nodeFee(channelUpdate_de.feeBaseMsat, channelUpdate_de.feeProportionalMillionths, amount_de)
|
||||
|
||||
val expiry_cd = expiry_de + channelUpdate_de.cltvExpiryDelta
|
||||
val amount_cd = amount_de + fee_d
|
||||
val fee_c = nodeFee(channelUpdate_cd.feeBaseMsat, channelUpdate_cd.feeProportionalMillionths, amount_cd)
|
||||
|
||||
val expiry_bc = expiry_cd + channelUpdate_cd.cltvExpiryDelta
|
||||
val amount_bc = amount_cd + fee_c
|
||||
val fee_b = nodeFee(channelUpdate_bc.feeBaseMsat, channelUpdate_bc.feeProportionalMillionths, amount_bc)
|
||||
|
||||
val expiry_ab = expiry_bc + channelUpdate_bc.cltvExpiryDelta
|
||||
val amount_ab = amount_bc + fee_b
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.Status.Failure
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.TestConstants.Alice
|
||||
import fr.acinq.eclair.channel.CMD_FULFILL_HTLC
|
||||
import fr.acinq.eclair.wire.UpdateAddHtlc
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuiteLike
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by PM on 24/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
|
||||
|
||||
test("LocalPaymentHandler should send an event when receiving a payment") {
|
||||
val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams))
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
val amountMsat = MilliSatoshi(42000)
|
||||
sender.send(handler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
val add = UpdateAddHtlc("11" * 32, 0, amountMsat.amount, pr.paymentHash, 0, "")
|
||||
sender.send(handler, add)
|
||||
sender.expectMsgType[CMD_FULFILL_HTLC]
|
||||
eventListener.expectMsg(PaymentReceived(amountMsat, add.paymentHash))
|
||||
}
|
||||
|
||||
test("Payment request generation should fail when the amount asked in not valid") {
|
||||
val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams))
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentReceived])
|
||||
|
||||
// negative amount should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(-50), "1 coffee"))
|
||||
val negativeError = sender.expectMsgType[Failure]
|
||||
assert(negativeError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// amount = 0 should fail
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(0), "1 coffee"))
|
||||
val zeroError = sender.expectMsgType[Failure]
|
||||
assert(zeroError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// large amount should fail (> 42.95 mBTC)
|
||||
sender.send(handler, ReceivePayment(Satoshi(1) + PaymentRequest.maxAmount, "1 coffee"))
|
||||
val largeAmountError = sender.expectMsgType[Failure]
|
||||
assert(largeAmountError.cause.getMessage.contains("amount is not valid"))
|
||||
|
||||
// success with 1 mBTC
|
||||
sender.send(handler, ReceivePayment(MilliSatoshi(100000000L), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
assert(pr.amount == Some(MilliSatoshi(100000000L)) && pr.nodeId.toString == Alice.nodeParams.privateKey.publicKey.toString)
|
||||
}
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.MilliSatoshi
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.channel.Register.ForwardShortId
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||
|
||||
val initialBlockCount = 420000
|
||||
Globals.blockCount.set(initialBlockCount)
|
||||
|
||||
test("payment failed (route not found)") { case (router, _) =>
|
||||
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
|
||||
val request = SendPayment(142000L, "42" * 32, f)
|
||||
sender.send(paymentFSM, request)
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
||||
sender.expectMsg(PaymentFailed(request.paymentHash, LocalFailure(RouteNotFound) :: Nil))
|
||||
}
|
||||
|
||||
test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { case (router, _) =>
|
||||
val relayer = TestProbe()
|
||||
val routerForwarder = TestProbe()
|
||||
val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
|
||||
val request = SendPayment(142000L, "42" * 32, d, maxAttempts = 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, _, Nil) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.forward(router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData
|
||||
|
||||
relayer.expectMsg(ForwardShortId(channelId_ab, cmd1))
|
||||
sender.send(paymentFSM, UpdateFailMalformedHtlc("00" * 32, 0, "42" * 32, FailureMessageCodecs.BADONION))
|
||||
|
||||
// then the payment lifecycle will ask for a new route excluding the channel
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set(channelId_ab)))
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
|
||||
}
|
||||
|
||||
test("payment failed (TemporaryChannelFailure)") { case (router, _) =>
|
||||
val relayer = TestProbe()
|
||||
val routerForwarder = TestProbe()
|
||||
val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
|
||||
val request = SendPayment(142000L, "42" * 32, d, maxAttempts = 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, _, Nil) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.forward(router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData
|
||||
|
||||
val failure = TemporaryChannelFailure(channelUpdate_bc)
|
||||
|
||||
relayer.expectMsg(ForwardShortId(channelId_ab, cmd1))
|
||||
sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure)))
|
||||
|
||||
// payment lifecycle will ask the router to temporarily exclude this channel from its route calculations
|
||||
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c)))
|
||||
routerForwarder.forward(router)
|
||||
// payment lifecycle forwards the embedded channelUpdate to the router
|
||||
routerForwarder.expectMsg(channelUpdate_bc)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.forward(router)
|
||||
// we allow 2 tries, so we send a 2nd request to the router
|
||||
sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil))
|
||||
}
|
||||
|
||||
test("payment failed (PermanentChannelFailure)") { case (router, _) =>
|
||||
val relayer = TestProbe()
|
||||
val routerForwarder = TestProbe()
|
||||
val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
|
||||
val request = SendPayment(142000L, "42" * 32, d, maxAttempts = 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, _, Nil) = paymentFSM.stateData
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
routerForwarder.forward(router)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
|
||||
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData
|
||||
|
||||
val failure = PermanentChannelFailure
|
||||
|
||||
relayer.expectMsg(ForwardShortId(channelId_ab, cmd1))
|
||||
sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure)))
|
||||
|
||||
// payment lifecycle forwards the embedded channelUpdate to the router
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set(channelId_bc)))
|
||||
routerForwarder.forward(router)
|
||||
// we allow 2 tries, so we send a 2nd request to the router, which won't find another route
|
||||
|
||||
sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil))
|
||||
}
|
||||
|
||||
test("payment succeeded") { case (router, _) =>
|
||||
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
|
||||
val request = SendPayment(142000L, "42" * 32, d)
|
||||
sender.send(paymentFSM, request)
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
||||
sender.send(paymentFSM, UpdateFulfillHtlc("00" * 32, 0, "42" * 32))
|
||||
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
val PaymentSent(MilliSatoshi(request.amountMsat), feesPaid, request.paymentHash) = eventListener.expectMsgType[PaymentSent]
|
||||
assert(feesPaid.amount > 0)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,183 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{Amount, ExtraHop, RoutingInfoTag}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by fabrice on 15/05/17.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class PaymentRequestSpec extends FunSuite {
|
||||
|
||||
val priv = PrivateKey(BinaryData("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"), compressed = true)
|
||||
val pub = priv.publicKey
|
||||
val nodeId = pub
|
||||
assert(nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
|
||||
test("check minimal unit is used") {
|
||||
assert('p' === Amount.unit(MilliSatoshi(1)))
|
||||
assert('p' === Amount.unit(MilliSatoshi(99)))
|
||||
assert('n' === Amount.unit(MilliSatoshi(100)))
|
||||
assert('p' === Amount.unit(MilliSatoshi(101)))
|
||||
assert('n' === Amount.unit(Satoshi(1)))
|
||||
assert('u' === Amount.unit(Satoshi(100)))
|
||||
assert('n' === Amount.unit(Satoshi(101)))
|
||||
assert('u' === Amount.unit(Satoshi(1155400)))
|
||||
assert('m' === Amount.unit(MilliBtc(1)))
|
||||
assert('m' === Amount.unit(MilliBtc(10)))
|
||||
assert('m' === Amount.unit(Btc(1)))
|
||||
}
|
||||
|
||||
test("check that we can still decode non-minimal amount encoding") {
|
||||
assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000u"))
|
||||
assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000000n"))
|
||||
assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000000000p"))
|
||||
}
|
||||
|
||||
test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") {
|
||||
val ref = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount.isEmpty)
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Left("Please consider supporting this project"))
|
||||
assert(pr.fallbackAddress === None)
|
||||
assert(pr.tags.size === 2)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("Please send $3 for a cup of coffee to the same peer, within 1 minute") {
|
||||
val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(250000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Left("1 cup coffee"))
|
||||
assert(pr.fallbackAddress === None)
|
||||
assert(pr.tags.size === 3)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("Now send $24 for an entire list of things (hashed)") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === None)
|
||||
assert(pr.tags.size === 2)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") {
|
||||
val ref = "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf237cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqpqlhssu04sucpnz4axcv2dstmknqq6jsk2l"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lntb")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP"))
|
||||
assert(pr.tags.size == 3)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqqqqqqq7qqzqfnlkwydm8rg30gjku7wmxmk06sevjp53fmvrcfegvwy7d5443jvyhxsel0hulkstws7vqv400q4j3wgpk4crg49682hr4scqvmad43cqd5m7tf"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount === Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T"))
|
||||
assert(pr.routingInfo() === List(RoutingInfoTag(List(ExtraHop(PublicKey("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 72623859790382856L, 20, 3), ExtraHop(PublicKey("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), 217304205466536202L, 30, 4)))))
|
||||
assert(BinaryData(Protocol.writeUInt64(72623859790382856L, ByteOrder.BIG_ENDIAN)) == BinaryData("0102030405060708"))
|
||||
assert(BinaryData(Protocol.writeUInt64(217304205466536202L, ByteOrder.BIG_ENDIAN)) == BinaryData("030405060708090a"))
|
||||
assert(pr.tags.size == 4)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
|
||||
test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX"))
|
||||
assert(pr.tags.size == 3)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"))
|
||||
assert(pr.tags.size == 3)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
|
||||
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") {
|
||||
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"))
|
||||
assert(pr.tags.size == 3)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") {
|
||||
val ref = "lnbc20m1pvjluezcqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q90qkf3gd7fcqs0ewr7t3xf72ptmc4n38evg0xhy4p64nlg7hgrmq6g997tkrvezs8afs0x0y8v4vs8thwsk6knkvdfvfa7wmhhpcsxcqw0ny48"
|
||||
val pr = PaymentRequest.read(ref)
|
||||
assert(pr.prefix == "lnbc")
|
||||
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
|
||||
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
|
||||
assert(pr.timestamp == 1496314658L)
|
||||
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
|
||||
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
|
||||
assert(pr.fallbackAddress === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"))
|
||||
assert(pr.minFinalCltvExpiry === Some(12))
|
||||
assert(pr.tags.size == 4)
|
||||
assert(PaymentRequest.write(pr.sign(priv)) == ref)
|
||||
}
|
||||
|
||||
test("expiry is a variable-length unsigned value") {
|
||||
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(MilliSatoshi(100000L)), BinaryData("0001020304050607080900010203040506070809000102030405060708090102"),
|
||||
priv, "test", fallbackAddress = None, expirySeconds = Some(21600), timestamp = System.currentTimeMillis() / 1000L)
|
||||
|
||||
val serialized = PaymentRequest write pr
|
||||
val pr1 = PaymentRequest read serialized
|
||||
assert(pr.expiry === Some(21600))
|
||||
}
|
||||
}
|
||||
@ -1,232 +0,0 @@
|
||||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
// let's reuse the existing test data
|
||||
import HtlcGenerationSpec._
|
||||
|
||||
type FixtureParam = Tuple3[ActorRef, TestProbe, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
|
||||
within(30 seconds) {
|
||||
val register = TestProbe()
|
||||
val paymentHandler = TestProbe()
|
||||
// we are node B in the route A -> B -> C -> ....
|
||||
val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(privateKey = priv_b), register.ref, paymentHandler.ref))
|
||||
test((relayer, register, paymentHandler))
|
||||
}
|
||||
}
|
||||
|
||||
// node c is the next node in the route
|
||||
val nodeId_a = PublicKey(a)
|
||||
val nodeId_c = PublicKey(c)
|
||||
val channelId_ab: BinaryData = "65514354" * 8
|
||||
val channelId_bc: BinaryData = "64864544" * 8
|
||||
val channel_flags = 0x00.toByte
|
||||
|
||||
def makeCommitments(channelId: BinaryData) = Commitments(null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId)
|
||||
|
||||
test("relay an htlc-add") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
|
||||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, register, paymentHandler) =>
|
||||
|
||||
// TODO: we should use the new update_fail_malformed_htlc message (see BOLT 2)
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, "00" * Sphinx.PacketLength)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_MALFORMED_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.onionHash == Crypto.sha256(add_ab.onionRoutingPacket))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when amount is below the next hop's requirements") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, secrets) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0))))
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(AmountBelowMinimum(cmd.amountMsat, channelUpdate_bc)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when expiry does not match next hop's requirements") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0)))
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(IncorrectCltvExpiry(cmd.expiry, channelUpdate_bc)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when expiry is too soon") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, 0, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(ExpiryTooSoon(channelUpdate_bc)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail an htlc-add at the final node when amount has been modified by second-to-last node") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
|
||||
val hops1 = hops.head :: Nil
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc with a wrong expiry
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(FinalIncorrectHtlcAmount(add_ab.amountMsat)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail an htlc-add at the final node when expiry has been modified by second-to-last node") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
|
||||
val hops1 = hops.head :: Nil
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc with a wrong expiry
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry - 1, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(FinalIncorrectCltvExpiry(add_ab.expiry)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("relay an htlc-fulfill") { case (relayer, register, _) =>
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
|
||||
// there isn't any corresponding htlc, it does not matter here
|
||||
val fulfill_cb = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = "00" * 32)
|
||||
val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L)
|
||||
sender.send(relayer, ForwardFulfill(fulfill_cb, origin))
|
||||
|
||||
val fwd = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
|
||||
assert(fwd.channelId === origin.originChannelId)
|
||||
assert(fwd.message.id === origin.originHtlcId)
|
||||
|
||||
eventListener.expectMsg(PaymentRelayed(MilliSatoshi(origin.amountMsatIn), MilliSatoshi(origin.amountMsatOut), Crypto.sha256(fulfill_cb.paymentPreimage)))
|
||||
}
|
||||
|
||||
test("relay an htlc-fail") { case (relayer, register, _) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// there isn't any corresponding htlc, it does not matter here
|
||||
val fail_cb = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.createErrorPacket(BinaryData("01" * 32), TemporaryChannelFailure(channelUpdate_cd)))
|
||||
val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L)
|
||||
sender.send(relayer, ForwardFail(fail_cb, origin))
|
||||
|
||||
val fwd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
|
||||
assert(fwd.channelId === origin.originChannelId)
|
||||
assert(fwd.message.id === origin.originHtlcId)
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block}
|
||||
import fr.acinq.eclair.TestConstants.Alice
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.router.Announcements._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class AnnouncementsSpec extends FunSuite {
|
||||
|
||||
test("check nodeId1/nodeId2 lexical ordering") {
|
||||
val node1 = PublicKey("027710df7a1d7ad02e3572841a829d141d9f56b17de9ea124d2f83ea687b2e0461")
|
||||
val node2 = PublicKey("0306a730778d55deec162a74409e006034a24c46d541c67c6c45f89a2adde3d9b4")
|
||||
// NB: node1 < node2
|
||||
assert(isNode1(node1.toBin, node2.toBin))
|
||||
assert(!isNode1(node2.toBin, node1.toBin))
|
||||
}
|
||||
|
||||
test("create valid signed channel announcement") {
|
||||
val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey, randomKey, randomKey, randomKey)
|
||||
val (node_a_sig, bitcoin_a_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, 42, node_a, node_b.publicKey, bitcoin_a, bitcoin_b.publicKey, "")
|
||||
val (node_b_sig, bitcoin_b_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, 42, node_b, node_a.publicKey, bitcoin_b, bitcoin_a.publicKey, "")
|
||||
val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, 42, node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig)
|
||||
assert(checkSigs(ann))
|
||||
assert(checkSigs(ann.copy(nodeId1 = randomKey.publicKey)) === false)
|
||||
}
|
||||
|
||||
test("create valid signed node announcement") {
|
||||
val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses)
|
||||
assert(checkSig(ann))
|
||||
assert(checkSig(ann.copy(timestamp = 153)) === false)
|
||||
}
|
||||
|
||||
test("create valid signed channel update announcement") {
|
||||
val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey.publicKey, 45561, Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
|
||||
assert(checkSig(ann, Alice.nodeParams.privateKey.publicKey))
|
||||
assert(checkSig(ann, randomKey.publicKey) === false)
|
||||
}
|
||||
|
||||
test("check flags") {
|
||||
val node1_priv = PrivateKey("5f447b05d86de82de6b245a65359d22f844ae764e2ae3824ac4ace7d8e1c749b01")
|
||||
val node2_priv = PrivateKey("eff467c5b601fdcc07315933767013002cd0705223d8e526cbb0c1bc75ccb62901")
|
||||
// NB: node1 < node2 (public keys)
|
||||
assert(isNode1(node1_priv.publicKey.toBin, node2_priv.publicKey.toBin))
|
||||
assert(!isNode1(node2_priv.publicKey.toBin, node1_priv.publicKey.toBin))
|
||||
val channelUpdate1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, 0, 0, 0, 0, 0, enable = true)
|
||||
val channelUpdate1_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node1_priv, node2_priv.publicKey, 0, 0, 0, 0, 0, enable = false)
|
||||
val channelUpdate2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, 0, 0, 0, 0, 0, enable = true)
|
||||
val channelUpdate2_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, node2_priv, node1_priv.publicKey, 0, 0, 0, 0, 0, enable = false)
|
||||
assert(channelUpdate1.flags == BinaryData("0000")) // ....00
|
||||
assert(channelUpdate1_disabled.flags == BinaryData("0002")) // ....10
|
||||
assert(channelUpdate2.flags == BinaryData("0001")) // ....01
|
||||
assert(channelUpdate2_disabled.flags == BinaryData("0003")) // ....11
|
||||
assert(isNode1(channelUpdate1.flags))
|
||||
assert(isNode1(channelUpdate1_disabled.flags))
|
||||
assert(!isNode1(channelUpdate2.flags))
|
||||
assert(!isNode1(channelUpdate2_disabled.flags))
|
||||
assert(isEnabled(channelUpdate1.flags))
|
||||
assert(!isEnabled(channelUpdate1_disabled.flags))
|
||||
assert(isEnabled(channelUpdate2.flags))
|
||||
assert(!isEnabled(channelUpdate2_disabled.flags))
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,266 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, PerHopPayload}
|
||||
import fr.acinq.eclair.{Globals, randomKey, toShortId}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class RouteCalculationSpec extends FunSuite {
|
||||
|
||||
val (a, b, c, d, e) = (randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
|
||||
|
||||
test("calculate simple route") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(3L, c, d),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
val route = Router.findRouteDijkstra(a, e, channels)
|
||||
assert(route.map(_.id) === 1 :: 2 :: 3 :: 4 :: Nil)
|
||||
|
||||
}
|
||||
|
||||
test("randomize routes") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, a, b),
|
||||
ChannelDesc(3L, b, c),
|
||||
ChannelDesc(4L, b, c),
|
||||
ChannelDesc(5L, c, d),
|
||||
ChannelDesc(6L, c, d),
|
||||
ChannelDesc(4L, d, e),
|
||||
ChannelDesc(5L, d, e)
|
||||
)
|
||||
|
||||
val routes = for (i <- 0 until 10) yield Router.findRouteDijkstra(a, e, channels)
|
||||
assert(routes.exists(_ != routes.head))
|
||||
|
||||
}
|
||||
|
||||
test("no local channels") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
val exc = intercept[RuntimeException] {
|
||||
Router.findRouteDijkstra(a, e, channels)
|
||||
}
|
||||
assert(exc == RouteNotFound)
|
||||
}
|
||||
|
||||
test("route not found") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
val exc = intercept[RuntimeException] {
|
||||
Router.findRouteDijkstra(a, e, channels)
|
||||
}
|
||||
assert(exc == RouteNotFound)
|
||||
}
|
||||
|
||||
test("route not found (unknown destination)") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(3L, c, d)
|
||||
)
|
||||
|
||||
val exc = intercept[RuntimeException] {
|
||||
Router.findRouteDijkstra(a, e, channels)
|
||||
}
|
||||
assert(exc == RouteNotFound)
|
||||
}
|
||||
|
||||
test("route to self") {
|
||||
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(3L, c, d),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
val exc = intercept[RuntimeException] {
|
||||
Router.findRouteDijkstra(a, a, channels)
|
||||
}
|
||||
assert(exc == CannotRouteToSelf)
|
||||
}
|
||||
|
||||
test("route to immediate neighbor") {
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(3L, c, d),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
val route = Router.findRouteDijkstra(a, b, channels)
|
||||
assert(route.map(_.id) === 1 :: Nil)
|
||||
}
|
||||
|
||||
test("directed graph") {
|
||||
val channels = List(
|
||||
ChannelDesc(1L, a, b),
|
||||
ChannelDesc(2L, b, c),
|
||||
ChannelDesc(3L, c, d),
|
||||
ChannelDesc(4L, d, e)
|
||||
)
|
||||
|
||||
// a->e works, e->a fails
|
||||
|
||||
Router.findRouteDijkstra(a, e, channels)
|
||||
|
||||
intercept[RuntimeException] {
|
||||
Router.findRouteDijkstra(e, a, channels)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test("compute an example sig") {
|
||||
val data = BinaryData("00" * 32)
|
||||
val key = PrivateKey(BinaryData("11" * 32))
|
||||
val sig = Crypto.encodeSignature(Crypto.sign(data, key))
|
||||
assert(Crypto.isDERSignature(sig :+ 1.toByte))
|
||||
}
|
||||
|
||||
test("calculate route and return metadata") {
|
||||
|
||||
val DUMMY_SIG = BinaryData("3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201")
|
||||
|
||||
val uab = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 1L, 0L, "0000", 1, 42, 2500, 140)
|
||||
val uba = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 1L, 1L, "0001", 1, 43, 2501, 141)
|
||||
val ubc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 2L, 1L, "0000", 1, 44, 2502, 142)
|
||||
val ucb = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 2L, 1L, "0001", 1, 45, 2503, 143)
|
||||
val ucd = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 3L, 1L, "0000", 1, 46, 2504, 144)
|
||||
val udc = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 3L, 1L, "0001", 1, 47, 2505, 145)
|
||||
val ude = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 4L, 1L, "0000", 1, 48, 2506, 146)
|
||||
val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, 4L, 1L, "0001", 1, 49, 2507, 147)
|
||||
|
||||
val updates = Map(
|
||||
ChannelDesc(1L, a, b) -> uab,
|
||||
ChannelDesc(1L, b, a) -> uba,
|
||||
ChannelDesc(2L, b, c) -> ubc,
|
||||
ChannelDesc(2L, c, b) -> ucb,
|
||||
ChannelDesc(3L, c, d) -> ucd,
|
||||
ChannelDesc(3L, d, c) -> udc,
|
||||
ChannelDesc(4L, d, e) -> ude,
|
||||
ChannelDesc(4L, e, d) -> ued
|
||||
)
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val hops = Await.result(Router.findRoute(a, e, updates), 3 seconds)
|
||||
|
||||
assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil)
|
||||
}
|
||||
|
||||
test("calculate route with extra hops") {
|
||||
// E (sender) -> D - public -> C - private -> B - private -> A (receiver)
|
||||
|
||||
val amount = MilliSatoshi(100000000L)
|
||||
val paymentPreimage = BinaryData("0" * 32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val privateKey = PrivateKey("bb77027e3b6ef55f3b16eb6973d124f68e0c2afc16accc00a44ec6b3d1e58cc601")
|
||||
|
||||
// Ask router for a route from 02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8 (node C)
|
||||
// to 0299439d988cbf31388d59e3d6f9e184e7a0739b8b8fcdc298957216833935f9d3 (node A)
|
||||
val hopCB = Hop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"),
|
||||
PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"),
|
||||
ChannelUpdate("3044022075bc283539935b1bc126035ef98d0f9bcd5dd7b0832b0a6175dc14a5ee12d47102203d141a4da4f83fca9d65bddfb9ee6ea5cdfcdb364de062d1370500f511b8370701",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 24412456671576064L, 1509366313, BinaryData("0000"), 144, 1000, 546000, 10))
|
||||
|
||||
val hopBA = Hop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"),
|
||||
PublicKey("0299439d988cbf31388d59e3d6f9e184e7a0739b8b8fcdc298957216833935f9d3"),
|
||||
ChannelUpdate("304402205e9b28e26add5417ad97f6eb161229dd7db0d7848e146a1856a8841238bc627902203cc59996ca490375fd76a3327adfb7c5150ee3288ad1663b8c4fbe8908eb489a01",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 23366821113626624L, 1509455356, BinaryData("0001"), 144, 1000, 546000, 10))
|
||||
|
||||
val reverseRoute = List(hopBA, hopCB)
|
||||
val extraRoute = PaymentHop.buildExtra(reverseRoute, amount.amount)
|
||||
|
||||
assert(extraRoute === List(ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 547005, 144),
|
||||
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"), 23366821113626624L, 547000, 144)))
|
||||
|
||||
// Sender side
|
||||
|
||||
// Ask router for a route D -> C
|
||||
val hopDC = Hop(PublicKey("03c1b07dbe10e178216150b49646ded556466ed15368857fa721cf1acd9d9a6f24"),
|
||||
PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"),
|
||||
ChannelUpdate("3044022060c1034092d4e41d75271eb619ef0a0f00d0b5a61c4245e0f14eeac91a3c823202200da9c8b8067e73c32aea41cb9eec050ce49cb944877d9abb3b08be2dea92497301",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 24403660578553856L, 1509456040, BinaryData("0001"), 144, 1000, 546000, 10))
|
||||
|
||||
val (amt, expiry, payloads) = PaymentLifecycle.buildPayloads(amount.amount, 10, Seq(hopDC) ++ extraRoute)
|
||||
|
||||
assert(payloads === List(PerHopPayload(24403660578553856L, 101094005L, 298),
|
||||
PerHopPayload(24412456671576064L, 100547000L, 154), PerHopPayload(23366821113626624L, 100000000L, 10), PerHopPayload(0L, 100000000L, 10)))
|
||||
|
||||
assert(amt == 101641015L)
|
||||
assert(expiry == 442)
|
||||
}
|
||||
|
||||
test("stale channels pruning") {
|
||||
// set current block height
|
||||
Globals.blockCount.set(500000)
|
||||
|
||||
// we only care about timestamps
|
||||
def channelAnnouncement(shortChannelId: Long) = ChannelAnnouncement("", "", "", "", "", "", shortChannelId, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)
|
||||
|
||||
def channelUpdate(shortChannelId: Long, timestamp: Long) = ChannelUpdate("", "", shortChannelId, timestamp, "", 0, 0, 0, 0)
|
||||
|
||||
def desc(shortChannelId: Long) = ChannelDesc(shortChannelId, randomKey.publicKey, randomKey.publicKey)
|
||||
|
||||
def daysAgoInBlocks(daysAgo: Int): Int = Globals.blockCount.get().toInt - 144 * daysAgo
|
||||
|
||||
def daysAgoInSeconds(daysAgo: Int): Long = Platform.currentTime / 1000 - daysAgo * 24 * 3600
|
||||
|
||||
// a is an old channel with an old channel update => PRUNED
|
||||
val id_a = toShortId(daysAgoInBlocks(16), 0, 0)
|
||||
val chan_a = channelAnnouncement(id_a)
|
||||
val upd_a = channelUpdate(id_a, daysAgoInSeconds(30))
|
||||
// b is an old channel with no channel update => PRUNED
|
||||
val id_b = toShortId(daysAgoInBlocks(16), 1, 0)
|
||||
val chan_b = channelAnnouncement(id_b)
|
||||
// c is an old channel with a recent channel update => KEPT
|
||||
val id_c = toShortId(daysAgoInBlocks(16), 2, 0)
|
||||
val chan_c = channelAnnouncement(id_c)
|
||||
val upd_c = channelUpdate(id_c, daysAgoInSeconds(2))
|
||||
// d is a recent channel with a recent channel update => KEPT
|
||||
val id_d = toShortId(daysAgoInBlocks(2), 0, 0)
|
||||
val chan_d = channelAnnouncement(id_d)
|
||||
val upd_d = channelUpdate(id_d, daysAgoInSeconds(2))
|
||||
// e is a recent channel with no channel update => KEPT
|
||||
val id_e = toShortId(daysAgoInBlocks(1), 0, 0)
|
||||
val chan_e = channelAnnouncement(id_e)
|
||||
|
||||
val channels = Map(id_a -> chan_a, id_b -> chan_b, id_c -> chan_c, id_d -> chan_d, id_e -> chan_e)
|
||||
val updates = Map(desc(id_a) -> upd_a, desc(id_c) -> upd_c, desc(id_d) -> upd_d)
|
||||
|
||||
val staleChannels = Router.getStaleChannels(channels, updates).toSet
|
||||
|
||||
assert(staleChannels === Set(id_a, id_b))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import akka.actor.{ActorSystem, Terminated}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuiteLike
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Base class for router testing.
|
||||
* It is re-used in payment FSM tests
|
||||
* Created by PM on 29/08/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ThrottleForwarderSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
|
||||
|
||||
test("forward and delay messages, then dies") {
|
||||
|
||||
val target = TestProbe()
|
||||
val messages = 0 until 100
|
||||
|
||||
val forwarder = system.actorOf(ThrottleForwarder.props(target.ref, messages, 7, 5 millis))
|
||||
|
||||
target.watch(forwarder)
|
||||
messages.foreach(target.expectMsg(_))
|
||||
target.expectMsgType[Terminated]
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,139 +0,0 @@
|
||||
package fr.acinq.eclair.wire
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.channel.{LocalParams, RemoteParams}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.{Local, Relayed}
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.ChannelCodecs._
|
||||
import fr.acinq.eclair.{UInt64, randomKey}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 31/05/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ChannelCodecsSpec extends FunSuite {
|
||||
|
||||
def randomBytes(size: Int): BinaryData = {
|
||||
val bin = new Array[Byte](size)
|
||||
Random.nextBytes(bin)
|
||||
bin
|
||||
}
|
||||
|
||||
test("encode/decode localparams") {
|
||||
val o = LocalParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
dustLimitSatoshis = Random.nextInt(Int.MaxValue),
|
||||
maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)),
|
||||
channelReserveSatoshis = Random.nextInt(Int.MaxValue),
|
||||
htlcMinimumMsat = Random.nextInt(Int.MaxValue),
|
||||
toSelfDelay = Random.nextInt(Short.MaxValue),
|
||||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
fundingPrivKey = randomKey,
|
||||
revocationSecret = randomKey.value,
|
||||
paymentKey = randomKey,
|
||||
delayedPaymentKey = randomKey.value,
|
||||
htlcKey = randomKey,
|
||||
defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)),
|
||||
shaSeed = randomBytes(32),
|
||||
isFunder = Random.nextBoolean(),
|
||||
globalFeatures = randomBytes(256),
|
||||
localFeatures = randomBytes(256))
|
||||
val encoded = localParamsCodec.encode(o).require
|
||||
val decoded = localParamsCodec.decode(encoded).require
|
||||
assert(o === decoded.value)
|
||||
}
|
||||
|
||||
test("encode/decode remoteparams") {
|
||||
val o = RemoteParams(
|
||||
nodeId = randomKey.publicKey,
|
||||
dustLimitSatoshis = Random.nextInt(Int.MaxValue),
|
||||
maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)),
|
||||
channelReserveSatoshis = Random.nextInt(Int.MaxValue),
|
||||
htlcMinimumMsat = Random.nextInt(Int.MaxValue),
|
||||
toSelfDelay = Random.nextInt(Short.MaxValue),
|
||||
maxAcceptedHtlcs = Random.nextInt(Short.MaxValue),
|
||||
fundingPubKey = randomKey.publicKey,
|
||||
revocationBasepoint = randomKey.publicKey.value,
|
||||
paymentBasepoint = randomKey.publicKey.value,
|
||||
delayedPaymentBasepoint = randomKey.publicKey.value,
|
||||
htlcBasepoint = randomKey.publicKey.value,
|
||||
globalFeatures = randomBytes(256),
|
||||
localFeatures = randomBytes(256))
|
||||
val encoded = remoteParamsCodec.encode(o).require
|
||||
val decoded = remoteParamsCodec.decodeValue(encoded).require
|
||||
assert(o === decoded)
|
||||
}
|
||||
|
||||
test("encode/decode direction") {
|
||||
assert(directionCodec.decodeValue(directionCodec.encode(IN).require).require === IN)
|
||||
assert(directionCodec.decodeValue(directionCodec.encode(OUT).require).require === OUT)
|
||||
}
|
||||
|
||||
test("encode/decode htlc") {
|
||||
val add = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val htlc1 = DirectedHtlc(direction = IN, add = add)
|
||||
val htlc2 = DirectedHtlc(direction = OUT, add = add)
|
||||
assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1)
|
||||
assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require === htlc2)
|
||||
}
|
||||
|
||||
test("encode/decode commitment spec") {
|
||||
val add1 = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val add2 = UpdateAddHtlc(
|
||||
channelId = randomBytes(32),
|
||||
id = Random.nextInt(Int.MaxValue),
|
||||
amountMsat = Random.nextInt(Int.MaxValue),
|
||||
expiry = Random.nextInt(Int.MaxValue),
|
||||
paymentHash = randomBytes(32),
|
||||
onionRoutingPacket = randomBytes(Sphinx.PacketLength))
|
||||
val htlc1 = DirectedHtlc(direction = IN, add = add1)
|
||||
val htlc2 = DirectedHtlc(direction = OUT, add = add2)
|
||||
val htlcs = Set(htlc1, htlc2)
|
||||
assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require === htlcs)
|
||||
val o = CommitmentSpec(
|
||||
htlcs = Set(htlc1, htlc2),
|
||||
feeratePerKw = Random.nextInt(Int.MaxValue),
|
||||
toLocalMsat = Random.nextInt(Int.MaxValue),
|
||||
toRemoteMsat = Random.nextInt(Int.MaxValue)
|
||||
)
|
||||
val encoded = commitmentSpecCodec.encode(o).require
|
||||
val decoded = commitmentSpecCodec.decode(encoded).require
|
||||
assert(o === decoded.value)
|
||||
}
|
||||
|
||||
test("encode/decode origin") {
|
||||
assert(originCodec.decodeValue(originCodec.encode(Local(None)).require).require === Local(None))
|
||||
val relayed = Relayed(randomBytes(32), 4324, 12000000L, 11000000L)
|
||||
assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed)
|
||||
}
|
||||
|
||||
test("encode/decode map of origins") {
|
||||
val map = Map(
|
||||
1L -> Local(None),
|
||||
42L -> Relayed(randomBytes(32), 4324, 12000000L, 11000000L),
|
||||
130L -> Relayed(randomBytes(32), -45, 13000000L, 12000000L),
|
||||
1000L -> Relayed(randomBytes(32), 10, 14000000L, 13000000L),
|
||||
-32L -> Relayed(randomBytes(32), 54, 15000000L, 14000000L),
|
||||
-4L -> Local(None))
|
||||
assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map)
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-node-gui_2.11</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>eclair-node-gui</name>
|
||||
|
||||
<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>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<!-- we hide the git commit in the Specification-Version standard field-->
|
||||
<Specification-Version>${git.commit.id}</Specification-Version>
|
||||
<Url>${project.parent.url}</Url>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.chrisdchristo</groupId>
|
||||
<artifactId>capsule-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<appClass>fr.acinq.eclair.JavafxBoot</appClass>
|
||||
<type>fat</type>
|
||||
<fileName>${project.name}-${project.version}</fileName>
|
||||
<fileDesc>-${git.commit.id.abbrev}</fileDesc>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>installer</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.zenjava</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>8.8.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- required before build-native -->
|
||||
<id>create-jfxjar</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>build-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>build-native</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<vendor>ACINQ</vendor>
|
||||
<needShortcut>true</needShortcut>
|
||||
<appName>Eclair</appName>
|
||||
<nativeReleaseVersion>${project.version}</nativeReleaseVersion>
|
||||
<skipNativeVersionNumberSanitizing>true</skipNativeVersionNumberSanitizing>
|
||||
<nativeOutputDir>${project.build.directory}/jfx/installer</nativeOutputDir>
|
||||
<mainClass>fr.acinq.eclair.JavafxBoot</mainClass>
|
||||
<verbose>false</verbose>
|
||||
<bundler>EXE</bundler>
|
||||
<updateExistingJar>true</updateExistingJar>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair-node_${scala.version.short}</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 318 B |
Binary file not shown.
|
Before Width: | Height: | Size: 513 B |
Binary file not shown.
|
Before Width: | Height: | Size: 524 B |
Binary file not shown.
|
Before Width: | Height: | Size: 526 B |
Binary file not shown.
|
Before Width: | Height: | Size: 513 B |
@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import java.net.URL?>
|
||||
<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" prefWidth="590.0">
|
||||
<children>
|
||||
<GridPane styleClass="grid">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" maxWidth="250.0" minWidth="10.0"
|
||||
prefWidth="250.0"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="120.0"/>
|
||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
<RowConstraints minHeight="10.0" vgrow="SOMETIMES"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Amount to receive"/>
|
||||
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT"
|
||||
text="Maximum of ~0.042 BTC"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<TextField fx:id="amount" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
|
||||
<ComboBox fx:id="unit" GridPane.columnIndex="2" GridPane.rowIndex="0" GridPane.halignment="RIGHT">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<String fx:id="milliBTC" fx:value="milliBTC"/>
|
||||
<String fx:id="Satoshi" fx:value="Satoshi"/>
|
||||
<String fx:id="milliSatoshi" fx:value="milliSatoshi"/>
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<Label fx:id="amountError" opacity="0.0" styleClass="text-error, text-error-downward"
|
||||
text="Generic Invalid Amount"
|
||||
mouseTransparent="true" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
|
||||
|
||||
<VBox alignment="TOP_RIGHT" GridPane.rowIndex="1" GridPane.columnIndex="0">
|
||||
<children>
|
||||
<Label styleClass="text-strong" text="Optional description"/>
|
||||
<Label styleClass="label-description" wrapText="true" textAlignment="RIGHT"
|
||||
text="Can be left empty"/>
|
||||
</children>
|
||||
</VBox>
|
||||
<TextArea fx:id="description" GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2"
|
||||
wrapText="true" prefHeight="50.0"/>
|
||||
|
||||
<Button defaultButton="true" mnemonicParsing="false" onAction="#handleGenerate" prefHeight="29.0"
|
||||
prefWidth="95.0" text="Generate" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
<Button cancelButton="true" mnemonicParsing="false" onAction="#handleClose" styleClass="cancel"
|
||||
text="Close"
|
||||
GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="2" opacity="0"
|
||||
focusTraversable="false"/>
|
||||
|
||||
</children>
|
||||
</GridPane>
|
||||
<GridPane fx:id="resultBox" styleClass="grid, result-box" visible="false">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" maxWidth="250.0" minWidth="10.0" prefWidth="250.0"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="200.0" prefWidth="240.0"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" vgrow="ALWAYS"/>
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<ImageView fx:id="paymentRequestQRCode" fitWidth="250.0" pickOnBounds="true" preserveRatio="true"
|
||||
GridPane.rowIndex="0" GridPane.columnIndex="0"></ImageView>
|
||||
<VBox spacing="10.0" GridPane.rowIndex="0" GridPane.columnIndex="1">
|
||||
<children>
|
||||
<HBox spacing="10.0" alignment="CENTER_LEFT">
|
||||
<children>
|
||||
<Label text="Invoice:" styleClass="text-strong"/>
|
||||
<Button mnemonicParsing="false" onAction="#handleCopyInvoice"
|
||||
styleClass="copy-clipboard"
|
||||
text="Copy to Clipboard" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
</children>
|
||||
</HBox>
|
||||
<TextArea fx:id="paymentRequestTextArea" prefHeight="200.0" editable="false"
|
||||
styleClass="noteditable, text-sm, text-mono" wrapText="true"/>
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</GridPane>
|
||||
</children>
|
||||
<stylesheets>
|
||||
<URL value="@../commons/globals.css"/>
|
||||
<URL value="@../main/main.css"/>
|
||||
</stylesheets>
|
||||
</VBox>
|
||||
@ -1,29 +0,0 @@
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.io.File
|
||||
|
||||
import com.sun.javafx.application.LauncherImpl
|
||||
import fr.acinq.eclair.gui.{FxApp, FxPreloader}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
/**
|
||||
* Created by PM on 25/01/2016.
|
||||
*/
|
||||
object JavafxBoot extends App with Logging {
|
||||
val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair"))
|
||||
|
||||
try {
|
||||
val headless = System.getProperty("eclair.headless") != null
|
||||
|
||||
if (headless) {
|
||||
new Setup(datadir).bootstrap
|
||||
} else {
|
||||
LauncherImpl.launchApplication(classOf[FxApp], classOf[FxPreloader], Array(datadir.getAbsolutePath))
|
||||
}
|
||||
} catch {
|
||||
case t: Throwable =>
|
||||
System.err.println(s"fatal error: ${t.getMessage}")
|
||||
logger.error(s"fatal error: ${t.getMessage}")
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
import java.io.File
|
||||
import javafx.application.Preloader.ErrorNotification
|
||||
import javafx.application.{Application, Platform}
|
||||
import javafx.event.EventHandler
|
||||
import javafx.fxml.FXMLLoader
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.{Parent, Scene}
|
||||
import javafx.stage.{Popup, Screen, Stage, WindowEvent}
|
||||
|
||||
import akka.actor.{ActorSystem, Props, SupervisorStrategy}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.ElectrumEvent
|
||||
import fr.acinq.eclair.channel.ChannelEvent
|
||||
import fr.acinq.eclair.gui.controllers.{MainController, NotificationsController}
|
||||
import fr.acinq.eclair.payment.PaymentEvent
|
||||
import fr.acinq.eclair.router.NetworkEvent
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
|
||||
/**
|
||||
* Created by PM on 16/08/2016.
|
||||
*/
|
||||
class FxApp extends Application with Logging {
|
||||
|
||||
override def init = {
|
||||
logger.debug("initializing application...")
|
||||
}
|
||||
|
||||
def onError(t: Throwable): Unit = t match {
|
||||
case TCPBindException(port) =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
|
||||
case BitcoinRPCConnectionException =>
|
||||
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct."))
|
||||
case BitcoinZMQConnectionTimeoutException =>
|
||||
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct."))
|
||||
case IncompatibleDBException =>
|
||||
notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
|
||||
case t: Throwable =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
|
||||
}
|
||||
|
||||
override def start(primaryStage: Stage): Unit = {
|
||||
new Thread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
try {
|
||||
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
|
||||
primaryStage.getIcons.add(icon)
|
||||
val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml"))
|
||||
val pKit = Promise[Kit]()
|
||||
val handlers = new Handlers(pKit.future)
|
||||
val controller = new MainController(handlers, getHostServices)
|
||||
mainFXML.setController(controller)
|
||||
val mainRoot = mainFXML.load[Parent]
|
||||
val datadir = new File(getParameters.getUnnamed.get(0))
|
||||
implicit val system = ActorSystem("system")
|
||||
val setup = new Setup(datadir)
|
||||
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvent])
|
||||
setup.system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent])
|
||||
pKit.completeWith(setup.bootstrap)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
pKit.future.onComplete {
|
||||
case Success(_) =>
|
||||
Platform.runLater(new Runnable {
|
||||
override def run(): Unit = {
|
||||
val scene = new Scene(mainRoot)
|
||||
primaryStage.setTitle("Eclair")
|
||||
primaryStage.setMinWidth(600)
|
||||
primaryStage.setWidth(960)
|
||||
primaryStage.setMinHeight(400)
|
||||
primaryStage.setHeight(640)
|
||||
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {
|
||||
override def handle(event: WindowEvent) {
|
||||
System.exit(0)
|
||||
}
|
||||
})
|
||||
controller.initInfoFields(setup)
|
||||
primaryStage.setScene(scene)
|
||||
primaryStage.show
|
||||
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
|
||||
initNotificationStage(primaryStage, handlers)
|
||||
}
|
||||
})
|
||||
case Failure(t) => onError(t)
|
||||
}
|
||||
} catch {
|
||||
case t: Throwable => onError(t)
|
||||
}
|
||||
}
|
||||
}).start
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the notification stage and assign it to the handler class.
|
||||
*
|
||||
* @param owner stage owning the notification stage
|
||||
* @param notifhandlers Handles the notifications
|
||||
*/
|
||||
private def initNotificationStage(owner: Stage, notifhandlers: Handlers) = {
|
||||
// get fxml/controller
|
||||
val notifFXML = new FXMLLoader(getClass.getResource("/gui/main/notifications.fxml"))
|
||||
val notifsController = new NotificationsController
|
||||
notifFXML.setController(notifsController)
|
||||
val root = notifFXML.load[Parent]
|
||||
|
||||
Platform.runLater(new Runnable() {
|
||||
override def run = {
|
||||
// create scene
|
||||
val popup = new Popup
|
||||
popup.setHideOnEscape(false)
|
||||
popup.setAutoFix(false)
|
||||
val margin = 10
|
||||
val width = 300
|
||||
popup.setWidth(margin + width)
|
||||
popup.getContent.add(root)
|
||||
// positioning the popup @ TOP RIGHT of screen
|
||||
val screenBounds = Screen.getPrimary.getVisualBounds
|
||||
popup.show(owner, screenBounds.getMaxX - (margin + width), screenBounds.getMinY + margin)
|
||||
notifhandlers.initNotifications(notifsController)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
package fr.acinq.eclair.gui
|
||||
|
||||
import java.io.{File, FileWriter}
|
||||
import java.net.InetSocketAddress
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.gui.controllers._
|
||||
import fr.acinq.eclair.gui.utils.GUIValidators
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import fr.acinq.eclair.payment._
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Created by PM on 16/08/2016.
|
||||
*/
|
||||
class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends Logging {
|
||||
|
||||
implicit val timeout = Timeout(30 seconds)
|
||||
|
||||
private var notifsController: Option[NotificationsController] = None
|
||||
|
||||
def initNotifications(controller: NotificationsController) = {
|
||||
notifsController = Option(controller)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a connection to a node. If the channel option exists this will also open a channel with the node, with a
|
||||
* `fundingSatoshis` capacity and `pushMsat` amount.
|
||||
*
|
||||
* @param hostPort
|
||||
* @param channel
|
||||
*/
|
||||
def open(hostPort: String, channel: Option[NewChannel]) = {
|
||||
hostPort match {
|
||||
case GUIValidators.hostRegex(remoteNodeId, host, port) =>
|
||||
logger.info(s"opening a channel with remoteNodeId=$remoteNodeId")
|
||||
(for {
|
||||
address <- Future(new InetSocketAddress(host, port.toInt))
|
||||
pubkey = PublicKey(remoteNodeId)
|
||||
kit <- fKit
|
||||
conn <- kit.switchboard ? NewConnection(pubkey, address, channel)
|
||||
} yield conn) onFailure {
|
||||
case t =>
|
||||
notification("Connection failed", s"$host:$port", NOTIFICATION_ERROR)
|
||||
}
|
||||
case _ => {}
|
||||
}
|
||||
}
|
||||
|
||||
def send(nodeId: PublicKey, paymentHash: BinaryData, amountMsat: Long, minFinalCltvExpiry: Option[Long]) = {
|
||||
logger.info(s"sending $amountMsat to $paymentHash @ $nodeId")
|
||||
val request = minFinalCltvExpiry match {
|
||||
case None => SendPayment(amountMsat, paymentHash, nodeId)
|
||||
case Some(value) => SendPayment(amountMsat, paymentHash, nodeId, value)
|
||||
}
|
||||
(for {
|
||||
kit <- fKit
|
||||
res <- (kit.paymentInitiator ? request).mapTo[PaymentResult]
|
||||
} yield res)
|
||||
.onComplete {
|
||||
case Success(_: PaymentSucceeded) =>
|
||||
val message = s"${NumberFormat.getInstance(Locale.getDefault).format(amountMsat / 1000)} satoshis"
|
||||
notification("Payment Sent", message, NOTIFICATION_SUCCESS)
|
||||
case Success(PaymentFailed(_, failures)) =>
|
||||
val message = s"${
|
||||
failures.lastOption match {
|
||||
case Some(LocalFailure(t)) => t.getMessage
|
||||
case Some(RemoteFailure(_, e)) => e.failureMessage
|
||||
case _ => "Unknown error"
|
||||
}
|
||||
} (${failures.size} attempts)"
|
||||
notification("Payment Failed", message, NOTIFICATION_ERROR)
|
||||
case Failure(t) =>
|
||||
val message = t.getMessage
|
||||
notification("Payment Failed", message, NOTIFICATION_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
def receive(amountMsat: MilliSatoshi, description: String): Future[String] = for {
|
||||
kit <- fKit
|
||||
res <- (kit.paymentHandler ? ReceivePayment(amountMsat, description)).mapTo[PaymentRequest].map(PaymentRequest.write(_))
|
||||
} yield res
|
||||
|
||||
|
||||
def exportToDot(file: File) = for {
|
||||
kit <- fKit
|
||||
dot <- (kit.router ? 'dot).mapTo[String]
|
||||
_ = printToFile(file)(writer => writer.write(dot))
|
||||
} yield {}
|
||||
|
||||
private def printToFile(f: java.io.File)(op: java.io.FileWriter => Unit) {
|
||||
val p = new FileWriter(f)
|
||||
try {
|
||||
op(p)
|
||||
} finally {
|
||||
p.close
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a system notification if the system supports it.
|
||||
*
|
||||
* @param title Title of the notification
|
||||
* @param message main message of the notification, will not wrap
|
||||
* @param notificationType type of the message, default to NONE
|
||||
* @param showAppName true if you want the notification title to be preceded by "Eclair - ". True by default
|
||||
*/
|
||||
def notification(title: String, message: String, notificationType: NotificationType = NOTIFICATION_NONE, showAppName: Boolean = true) = {
|
||||
notifsController.map(_.addNotification(if (showAppName) s"Eclair - $title" else title, message, notificationType))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user