mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-13 11:35:47 +01:00
Merge branch 'master' into wip-android
This commit is contained in:
commit
e946c1a466
42 changed files with 1053 additions and 316 deletions
63
README.md
63
README.md
|
@ -6,16 +6,17 @@
|
|||
|
||||
**Eclair** (french for Lightning) is a scala implementation of the Lightning Network. It can run with or without a GUI, and a JSON-RPC API is also available.
|
||||
|
||||
This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning] and [lnd].
|
||||
This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning) and [lnd](https://github.com/LightningNetwork/lnd).
|
||||
|
||||
---
|
||||
|
||||
:construction: Both the BOLTs and Eclair itself are a work in progress. Expect things to break/change!
|
||||
:construction: Both the BOLTs and Eclair itself are still a work in progress. Expect things to break/change!
|
||||
|
||||
:warning: Eclair currently only runs on regtest or testnet.
|
||||
:rotating_light: If you intend to run Eclair on mainnet:
|
||||
- Keep in mind that it is beta-quality software and **don't put too much money** in it
|
||||
- Eclair's JSON-RPC API should **NOT** be accessible from the outside world (similarly to Bitcoin Core API)
|
||||
- Specific [configuration instructions for mainnet](#mainnet-usage) are provided below (by default Eclair runs on testnet)
|
||||
|
||||
:rotating_light: We had reports of Eclair being tested on various segwit-enabled blockchains. Keep in mind that Eclair is still alpha quality software, by using it with actual coins you are putting your funds at risk!
|
||||
|
||||
---
|
||||
|
||||
## Lightning Network Specification Compliance
|
||||
|
@ -27,11 +28,12 @@ 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-alpha11](https://github.com/ACINQ/eclair/blob/v0.2-alpha11/README.md#installation)**.
|
||||
|
||||
### Configuring Bitcoin Core
|
||||
|
||||
Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-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+. We highly recommend that you use Bitcoin Core 0.16 and will soon drop support for older versions.
|
||||
:warning: Eclair requires Bitcoin Core 0.16.0 or higher. If you are upgrading an existing wallet, you need to create a new address and send all your funds to that address.
|
||||
|
||||
Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node.
|
||||
Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet.
|
||||
|
||||
Run bitcoind with the following minimal `bitcoin.conf`:
|
||||
```
|
||||
|
@ -42,21 +44,9 @@ rpcpassword=bar
|
|||
txindex=1
|
||||
zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
|
||||
# lines below only needed with Bitcoin Core 0.16+
|
||||
deprecatedrpc=addwitnessaddress
|
||||
addresstype=p2sh-segwit
|
||||
```
|
||||
|
||||
Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet.
|
||||
|
||||
On **__testnet__**, the addresstype of all your UTXOs needs to be `p2sh-of-p2wpkh`. This is the default addresstype starting with Bitcoin Core 0.16, which provides native segwit support. For earlier versions of Bitcoin Core, additional steps are necessary.
|
||||
|
||||
* for new wallets created with Bitcoin Core 0.16 or later, no additional steps are necessary.
|
||||
* for existing wallets migrated to Bitcoin Core 0.16 or later, you need to create a new address and send all your funds to that address.
|
||||
* if you are running Bitcoin 0.15.1 or earlier, you need to create a segwit address manually. To do this, use the debug console, create a new address with `getnewaddress`, import it as a witness address with `addwitnessaddress`, and send all your balance to this witness address. If you need to create and send funds manually, don't forget to create and specify a witness address for the change output (this option is available on the GUI once you set the `Enable coin control features` wallet option).
|
||||
|
||||
|
||||
### Installing Eclair
|
||||
|
||||
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).
|
||||
|
@ -90,7 +80,7 @@ Eclair reads its configuration file, and write its logs, to `~/.eclair` by defau
|
|||
To change your node's configuration, create a file named `eclair.conf` in `~/.eclair`. Here's an example configuration file:
|
||||
|
||||
```
|
||||
eclair.server.port=9735
|
||||
eclair.chain=testnet
|
||||
eclair.node-alias=eclair
|
||||
eclair.node-color=49daaa
|
||||
```
|
||||
|
@ -99,6 +89,7 @@ Here are some of the most common options:
|
|||
|
||||
name | description | default value
|
||||
-----------------------------|---------------------------------------------------------------------------------------|--------------
|
||||
eclair.chain | Which blockchain to use: *regtest*, *testnet* or *mainnet* | testnet
|
||||
eclair.server.port | Lightning TCP port | 9735
|
||||
eclair.api.enabled | Enable/disable the API | false. By default the API is disabled. If you want to enable it, you must set a password.
|
||||
eclair.api.port | API HTTP port | 8080
|
||||
|
@ -175,12 +166,34 @@ If you want to persist the data directory, you can make the volume to your host
|
|||
docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq\eclair
|
||||
```
|
||||
|
||||
## Mainnet usage
|
||||
|
||||
Following are the minimum configuration files you need to use for Bitcoin Core and Eclair.
|
||||
|
||||
### Bitcoin Core configuration
|
||||
|
||||
```
|
||||
testnet=0
|
||||
server=1
|
||||
rpcuser=<your-rpc-user-here>
|
||||
rpcpassword=<your-rpc-password-here>
|
||||
txindex=1
|
||||
zmqpubrawblock=tcp://127.0.0.1:29000
|
||||
zmqpubrawtx=tcp://127.0.0.1:29000
|
||||
addresstype=p2sh-segwit
|
||||
```
|
||||
|
||||
### Eclair configuration
|
||||
|
||||
```
|
||||
eclair.chain=mainnet
|
||||
eclair.bitcoind.rpcport=8332
|
||||
eclair.bitcoind.rpcuser=<your-bitcoin-core-rpc-user-here>
|
||||
eclair.bitcoind.rpcpassword=<your-bitcoin-core-rpc-passsword-here>
|
||||
```
|
||||
|
||||
|
||||
## Resources
|
||||
- [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja
|
||||
- [2] [Reaching The Ground With Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) by Rusty Russell
|
||||
- [3] [Lightning Network Explorer](https://explorer.acinq.co) - Explore testnet LN nodes you can connect to
|
||||
|
||||
[c-lightning]: https://github.com/ElementsProject/lightning
|
||||
[lnd]: https://github.com/LightningNetwork/lnd
|
||||
|
||||
|
|
|
@ -79,10 +79,10 @@
|
|||
<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>https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-x86_64-linux-gnu.tar.gz
|
||||
</bitcoind.url>
|
||||
<bitcoind.md5>c811c157d4d618f7d7f4b9f24834551c</bitcoind.md5>
|
||||
<bitcoind.sha1>3ab7e537bd00bf35e6a78fca108d0d886f8289c1</bitcoind.sha1>
|
||||
<bitcoind.md5>1375c9f908b0327d9772d4bff0d9f03f</bitcoind.md5>
|
||||
<bitcoind.sha1>d0b05b51e1d572c44ef5b2cabbfcb662679cf7cb</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
|
@ -93,10 +93,10 @@
|
|||
</os>
|
||||
</activation>
|
||||
<properties>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.14.0/bitcoin-0.14.0-osx64.tar.gz
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-osx64.tar.gz
|
||||
</bitcoind.url>
|
||||
<bitcoind.md5>1521e1d0901169004b9c1c9b552868b7</bitcoind.md5>
|
||||
<bitcoind.sha1>7216298f77162618f322fdf499f1f1b67a0048b7</bitcoind.sha1>
|
||||
<bitcoind.md5>fc10d5cb12a4c3905d97df33f249eb1c</bitcoind.md5>
|
||||
<bitcoind.sha1>3b1ab75439ca7a9b547827ec3e59c7e61c1f6fcd</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
<profile>
|
||||
|
@ -107,9 +107,9 @@
|
|||
</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>
|
||||
<bitcoind.url>https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-win64.zip</bitcoind.url>
|
||||
<bitcoind.md5>5b9034754752b7e1b3117eaa5434792e</bitcoind.md5>
|
||||
<bitcoind.sha1>90d72e25782a4b454f5f507a26a3cf0f53baecef</bitcoind.sha1>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
@ -136,7 +136,7 @@
|
|||
<dependency>
|
||||
<groupId>org.json4s</groupId>
|
||||
<artifactId>json4s-jackson_${scala.version.short}</artifactId>
|
||||
<version>3.5.2</version>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
<!-- BITCOIN -->
|
||||
<dependency>
|
||||
|
|
309
eclair-core/src/main/resources/electrum/servers_mainnet.json
Normal file
309
eclair-core/src/main/resources/electrum/servers_mainnet.json
Normal file
|
@ -0,0 +1,309 @@
|
|||
{
|
||||
"207.154.223.80": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"4cii7ryno5j3axe4.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"74.222.1.20": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"88.198.43.231": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"E-X.not.fyi": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"VPS.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"arihancckjge66iv.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"aspinall.io": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"bauerjda5hnedjam.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"bauerjhejlv6di7s.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"btc.asis.io": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"btc.cihar.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"btc.smsys.me": {
|
||||
"pruning": "-",
|
||||
"s": "995",
|
||||
"version": "1.2"
|
||||
},
|
||||
"cryptohead.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"daedalus.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"de.hamster.science": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"e.keff.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"elec.luggs.co": {
|
||||
"pruning": "-",
|
||||
"s": "443",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum-server.ninja": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.achow101.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.cutie.ga": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.leblancnet.us": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.meltingice.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.nute.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.poorcoding.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.qtornado.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum.vom-stausee.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrum0.snel.it": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrumx-core.1209k.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrumx.bot.nu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrumx.nmdps.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrumx.westeurope.cloudapp.azure.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"electrumxhqdsmlu.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"elx2018.mooo.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"helicarrier.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"hsmiths4fyqlw5xw.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"hsmiths5mjk6uijs.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"j5jfrdthqt5g25xz.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"kirsche.emzy.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"luggscoqbymhvnkp.onion": {
|
||||
"pruning": "-",
|
||||
"t": "80",
|
||||
"version": "1.2"
|
||||
},
|
||||
"ndnd.selfhost.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"ndndword5lpb7eex.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"node.arihanc.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"node.erratic.space": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"ozahtqwp25chjdjd.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"qtornadoklbgdyww.onion": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"rbx.curalle.ovh": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"ruuxwv74pjxms3ws.onion": {
|
||||
"pruning": "-",
|
||||
"s": "10042",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"s7clinmo4cazmhul.onion": {
|
||||
"pruning": "-",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"songbird.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
},
|
||||
"spv.48.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50003",
|
||||
"version": "1.2"
|
||||
},
|
||||
"tardis.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
eclair {
|
||||
|
||||
chain = "test" // "regtest" for regtest, "test" for testnet. Livenet is not supported.
|
||||
chain = "testnet" // "regtest" for regtest, "testnet" for testnet, "mainnet" for mainnet
|
||||
|
||||
server {
|
||||
public-ips = [] // external ips, will be announced on the network
|
||||
|
@ -42,9 +42,8 @@ eclair {
|
|||
local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries
|
||||
channel-flags = 0 // do not announce channels
|
||||
dust-limit-satoshis = 546
|
||||
default-feerate-per-kb = 20000 // default bitcoin core value
|
||||
|
||||
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
|
||||
max-htlc-value-in-flight-msat = 1000000000 // 10 mBTC
|
||||
htlc-minimum-msat = 1
|
||||
max-accepted-htlcs = 30
|
||||
|
||||
|
@ -52,8 +51,8 @@ eclair {
|
|||
max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%)
|
||||
|
||||
to-remote-delay-blocks = 144 // number of blocks that the other node's to-self outputs must be delayed (144 ~ 1 day)
|
||||
max-to-local-delay-blocks = 1000 // maximum number of blocks that we are ready to accept for our own delayed outputs (1000 ~ 1 week)
|
||||
mindepth-blocks = 2
|
||||
max-to-local-delay-blocks = 2000 // maximum number of blocks that we are ready to accept for our own delayed outputs (2000 ~ 2 weeks)
|
||||
mindepth-blocks = 3
|
||||
expiry-delta-blocks = 144
|
||||
|
||||
fee-base-msat = 1000
|
||||
|
@ -68,8 +67,7 @@ eclair {
|
|||
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
|
||||
router-broadcast-interval = 60 seconds // see BOLT #7
|
||||
|
||||
ping-interval = 30 seconds
|
||||
auto-reconnect = true
|
||||
|
|
|
@ -62,7 +62,6 @@ case class NodeParams(keyManager: KeyManager,
|
|||
pendingRelayDb: PendingRelayDb,
|
||||
paymentsDb: PaymentsDb,
|
||||
routerBroadcastInterval: FiniteDuration,
|
||||
routerValidateInterval: FiniteDuration,
|
||||
pingInterval: FiniteDuration,
|
||||
maxFeerateMismatch: Double,
|
||||
updateFeeMinDiffRatio: Double,
|
||||
|
@ -110,25 +109,32 @@ object NodeParams {
|
|||
}
|
||||
}
|
||||
|
||||
def makeChainHash(chain: String): BinaryData = {
|
||||
chain match {
|
||||
case "regtest" => Block.RegtestGenesisBlock.hash
|
||||
case "testnet" => Block.TestnetGenesisBlock.hash
|
||||
case "mainnet" => Block.LivenetGenesisBlock.hash
|
||||
case invalid => throw new RuntimeException(s"invalid chain '$invalid'")
|
||||
}
|
||||
}
|
||||
|
||||
def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager): NodeParams = {
|
||||
|
||||
datadir.mkdirs()
|
||||
|
||||
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 chainHash = makeChainHash(chain)
|
||||
|
||||
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(datadir, "eclair.sqlite")}")
|
||||
val chaindir = new File(datadir, chain)
|
||||
chaindir.mkdir()
|
||||
|
||||
val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "eclair.sqlite")}")
|
||||
val channelsDb = new SqliteChannelsDb(sqlite)
|
||||
val peersDb = new SqlitePeersDb(sqlite)
|
||||
val pendingRelayDb = new SqlitePendingRelayDb(sqlite)
|
||||
val paymentsDb = new SqlitePaymentsDb(sqlite)
|
||||
|
||||
val sqliteNetwork = DriverManager.getConnection(s"jdbc:sqlite:${new File(datadir, "network.sqlite")}")
|
||||
val sqliteNetwork = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "network.sqlite")}")
|
||||
val networkDb = new SqliteNetworkDb(sqliteNetwork)
|
||||
|
||||
val color = BinaryData(config.getString("node-color"))
|
||||
|
@ -173,7 +179,6 @@ object NodeParams {
|
|||
pendingRelayDb = pendingRelayDb,
|
||||
paymentsDb = paymentsDb,
|
||||
routerBroadcastInterval = FiniteDuration(config.getDuration("router-broadcast-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
|
||||
routerValidateInterval = FiniteDuration(config.getDuration("router-validate-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
|
||||
pingInterval = FiniteDuration(config.getDuration("ping-interval", TimeUnit.SECONDS), TimeUnit.SECONDS),
|
||||
maxFeerateMismatch = config.getDouble("max-feerate-mismatch"),
|
||||
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"),
|
||||
|
|
|
@ -55,9 +55,9 @@ class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefa
|
|||
|
||||
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
|
||||
val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir))
|
||||
val keyManager = new LocalKeyManager(seed)
|
||||
val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager)
|
||||
val chain = config.getString("chain")
|
||||
val keyManager = new LocalKeyManager(seed, NodeParams.makeChainHash(chain))
|
||||
val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager)
|
||||
|
||||
logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}")
|
||||
logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}")
|
||||
|
@ -87,14 +87,14 @@ class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefa
|
|||
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)
|
||||
val feeProvider = (nodeParams.chainHash, bitcoin) match {
|
||||
case (Block.RegtestGenesisBlock.hash, _) => new ConstantFeeProvider(defaultFeerates)
|
||||
case _ => new FallbackFeeProvider(new BitgoFeeProvider(nodeParams.chainHash) :: 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))
|
||||
Globals.feeratesPerKw.set(FeeratesPerKw(feerates))
|
||||
system.eventStream.publish(CurrentFeerates(Globals.feeratesPerKw.get))
|
||||
logger.info(s"current feeratesPerByte=${Globals.feeratesPerByte.get()}")
|
||||
})
|
||||
|
@ -108,8 +108,8 @@ class Setup(datadir: File, wallet_opt: Option[EclairWallet] = None, overrideDefa
|
|||
val wallet = bitcoin match {
|
||||
case _ if wallet_opt.isDefined => wallet_opt.get
|
||||
case Electrum(electrumClient) =>
|
||||
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(Block.TestnetGenesisBlock.hash)), "electrum-wallet")
|
||||
new ElectrumEclairWallet(electrumWallet)
|
||||
val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash)), "electrum-wallet")
|
||||
new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,9 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
|
|||
|
||||
import BitcoinCoreWallet._
|
||||
|
||||
def fundTransaction(hex: String, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
|
||||
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(changeAddress, lockUnspents)).map(json => {
|
||||
|
||||
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"
|
||||
|
@ -42,7 +43,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
|
|||
})
|
||||
}
|
||||
|
||||
def fundTransaction(tx: Transaction, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), changeAddress, lockUnspents)
|
||||
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 => {
|
||||
|
@ -66,36 +67,31 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
|
|||
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
JString(address) <- rpcClient.invoke("getnewaddress")
|
||||
// we want bitcoind to only use segwit addresses to avoid malleability issues
|
||||
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
|
||||
} yield segwitAddress
|
||||
} yield address
|
||||
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
|
||||
// partial funding tx
|
||||
val partialFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
for {
|
||||
// we create a new segwit change address (we don't want bitcoin core to use regular malleable outputs)
|
||||
JString(changeAddress) <- rpcClient.invoke("getnewaddress")
|
||||
JString(segwitChangeAddress) <- rpcClient.invoke("addwitnessaddress", changeAddress)
|
||||
_ = logger.debug(s"using segwitChangeAddress=$segwitChangeAddress")
|
||||
// partial funding tx
|
||||
partialFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = TxOut(amount, pubkeyScript) :: Nil,
|
||||
lockTime = 0)
|
||||
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
|
||||
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, segwitChangeAddress, lockUnspents = true)
|
||||
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, lockUnspents = true)
|
||||
// now let's sign the funding tx
|
||||
SignTransactionResponse(fundingTx, _) <- signTransaction(unsignedFundingTx)
|
||||
// there will probably be a change output, so we need to find which output is ours
|
||||
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript)
|
||||
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
|
||||
} yield MakeFundingTxResponse(fundingTx, outputIndex)
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
|
||||
.map(_ => true) // if bitcoind says OK, then we consider the tx successfully published
|
||||
.recoverWith { case JsonRPCError(e) =>
|
||||
logger.warn(s"txid=${tx.txid} error=$e")
|
||||
getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
logger.warn(s"txid=${tx.txid} error=$e")
|
||||
getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
|
||||
}
|
||||
.recover { case _ => true } // in all other cases we consider that the tx has been published
|
||||
|
||||
|
@ -106,7 +102,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
|
|||
object BitcoinCoreWallet {
|
||||
|
||||
// @formatter:off
|
||||
case class Options(changeAddress: String, lockUnspents: Boolean)
|
||||
case class Options(lockUnspents: Boolean)
|
||||
case class Utxo(txid: String, vout: Long)
|
||||
case class FundTransactionResponse(tx: Transaction, changepos: Int, feeSatoshis: Long)
|
||||
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
|
||||
|
|
|
@ -294,6 +294,7 @@ object ElectrumClient {
|
|||
|
||||
val RegtestGenesisHeader = makeHeader(0, Block.RegtestGenesisBlock.header)
|
||||
val TestnetGenesisHeader = makeHeader(0, Block.TestnetGenesisBlock.header)
|
||||
val LivenetGenesisHeader = makeHeader(0, Block.LivenetGenesisBlock.header)
|
||||
}
|
||||
|
||||
case class TransactionHistory(history: Seq[TransactionHistoryItem]) extends Response
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.net.InetSocketAddress
|
|||
import akka.actor.{Actor, ActorRef, FSM, Props, Terminated}
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockCount
|
||||
import org.json4s
|
||||
import org.json4s.JsonAST.{JObject, JString}
|
||||
import org.json4s.jackson.JsonMethods
|
||||
|
||||
|
@ -161,17 +162,18 @@ object ElectrumClientPool {
|
|||
|
||||
def readServerAddresses(stream: InputStream): Set[InetSocketAddress] = try {
|
||||
val JObject(values) = JsonMethods.parse(stream)
|
||||
val addresses = values.map {
|
||||
val addresses = values.flatMap {
|
||||
case (name, fields) =>
|
||||
val JString(port) = fields \ "t"
|
||||
new InetSocketAddress(name, port.toInt)
|
||||
fields \ "t" match {
|
||||
case JString(port) => Some(new InetSocketAddress(name, port.toInt))
|
||||
case _ => None // we only support raw TCP (not SSL) connection to electrum servers for now
|
||||
}
|
||||
}
|
||||
addresses.toSet
|
||||
} finally {
|
||||
stream.close()
|
||||
}
|
||||
|
||||
|
||||
// @formatter:off
|
||||
sealed trait State
|
||||
case object Disconnected extends State
|
||||
|
|
|
@ -18,7 +18,7 @@ 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.bitcoin.{Base58, Base58Check, BinaryData, Block, 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}
|
||||
|
@ -26,7 +26,7 @@ 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 {
|
||||
class ElectrumEclairWallet(val wallet: ActorRef, chainHash: BinaryData)(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)
|
||||
|
||||
|
@ -63,8 +63,11 @@ class ElectrumEclairWallet(val wallet: ActorRef)(implicit system: ActorSystem, e
|
|||
|
||||
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
|
||||
case (Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) if chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.TestnetGenesisBlock.hash => Script.pay2pkh(pubKeyHash)
|
||||
case (Base58.Prefix.PubkeyAddress, pubKeyHash) if chainHash == Block.LivenetGenesisBlock.hash => Script.pay2pkh(pubKeyHash)
|
||||
case (Base58.Prefix.ScriptAddressTestnet, scriptHash) if chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.TestnetGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil
|
||||
case (Base58.Prefix.ScriptAddress, scriptHash) if chainHash == Block.LivenetGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil
|
||||
case _ => throw new RuntimeException("payment address does not match our blockchain")
|
||||
}
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0)
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
|
|||
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
|
||||
val accountMaster = accountKey(master)
|
||||
val changeMaster = changeKey(master)
|
||||
val accountMaster = accountKey(master, chainHash)
|
||||
val changeMaster = changeKey(master, chainHash)
|
||||
|
||||
client ! ElectrumClient.AddStatusListener(self)
|
||||
|
||||
|
@ -87,6 +87,7 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
|
|||
val header = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash => ElectrumClient.Header.RegtestGenesisHeader
|
||||
case Block.TestnetGenesisBlock.hash => ElectrumClient.Header.TestnetGenesisHeader
|
||||
case Block.LivenetGenesisBlock.hash => ElectrumClient.Header.LivenetGenesisHeader
|
||||
}
|
||||
val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
|
||||
val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
|
||||
|
@ -139,7 +140,7 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
|
|||
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")
|
||||
log.info(s"received status=$status for scriptHash=$scriptHash key=${segwitAddress(key, chainHash)} isChange=$isChange")
|
||||
|
||||
// let's retrieve the tx history for this key
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
|
@ -149,7 +150,7 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
|
|||
// 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")
|
||||
log.info(s"generated key with index=${newKey.path.lastChildNumber} scriptHash=$newScriptHash key=${segwitAddress(newKey, chainHash)} 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)
|
||||
|
@ -341,15 +342,18 @@ object ElectrumWallet {
|
|||
* @param key public key
|
||||
* @return the address of the p2sh-of-p2wpkh script for this key
|
||||
*/
|
||||
def segwitAddress(key: PublicKey): String = {
|
||||
def segwitAddress(key: PublicKey, chainHash: BinaryData): String = {
|
||||
val script = Script.pay2wpkh(key)
|
||||
val hash = Crypto.hash160(Script.write(script))
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
|
||||
chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
|
||||
case Block.LivenetGenesisBlock.hash => Base58Check.encode(Base58.Prefix.ScriptAddress, hash)
|
||||
}
|
||||
}
|
||||
|
||||
def segwitAddress(key: ExtendedPrivateKey): String = segwitAddress(key.publicKey)
|
||||
def segwitAddress(key: ExtendedPrivateKey, chainHash: BinaryData): String = segwitAddress(key.publicKey, chainHash)
|
||||
|
||||
def segwitAddress(key: PrivateKey): String = segwitAddress(key.publicKey)
|
||||
def segwitAddress(key: PrivateKey, chainHash: BinaryData): String = segwitAddress(key.publicKey, chainHash)
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -369,17 +373,28 @@ object ElectrumWallet {
|
|||
* use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh
|
||||
*
|
||||
* @param master master key
|
||||
* @return the BIP49 account key for this master key: m/49'/1'/0'/0
|
||||
* @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet
|
||||
*/
|
||||
def accountKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
|
||||
def accountKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
|
||||
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 0L :: Nil)
|
||||
case Block.LivenetGenesisBlock.hash =>
|
||||
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: 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
|
||||
* @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet
|
||||
*/
|
||||
def changeKey(master: ExtendedPrivateKey) = DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
|
||||
def changeKey(master: ExtendedPrivateKey, chainHash: BinaryData) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash =>
|
||||
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(1) :: hardened(0) :: 1L :: Nil)
|
||||
case Block.LivenetGenesisBlock.hash =>
|
||||
DeterministicWallet.derivePrivateKey(master, hardened(49) :: hardened(0) :: hardened(0) :: 1L :: Nil)
|
||||
}
|
||||
|
||||
def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum)
|
||||
|
||||
|
@ -439,7 +454,8 @@ object ElectrumWallet {
|
|||
* @param pendingTransactionRequests requests pending a response from the electrum server
|
||||
* @param pendingTransactions transactions received but not yet connected to their parents
|
||||
*/
|
||||
case class Data(tip: ElectrumClient.Header,
|
||||
case class Data(chainHash: BinaryData,
|
||||
tip: ElectrumClient.Header,
|
||||
accountKeys: Vector[ExtendedPrivateKey],
|
||||
changeKeys: Vector[ExtendedPrivateKey],
|
||||
status: Map[BinaryData, String],
|
||||
|
@ -488,7 +504,7 @@ object ElectrumWallet {
|
|||
accountKeys.head
|
||||
}
|
||||
|
||||
def currentReceiveAddress = segwitAddress(currentReceiveKey)
|
||||
def currentReceiveAddress = segwitAddress(currentReceiveKey, chainHash)
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -503,7 +519,7 @@ object ElectrumWallet {
|
|||
changeKeys.head
|
||||
}
|
||||
|
||||
def currentChangeAddress = segwitAddress(currentChangeKey)
|
||||
def currentChangeAddress = segwitAddress(currentChangeKey, chainHash)
|
||||
|
||||
def isMine(txIn: TxIn): Boolean = extractPubKeySpentFrom(txIn).exists(pub => publicScriptMap.contains(Script.write(computePublicKeyScript(pub))))
|
||||
|
||||
|
@ -784,7 +800,7 @@ object ElectrumWallet {
|
|||
|
||||
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(), None)
|
||||
= Data(params.chainHash, tip, accountKeys, changeKeys, Map(), Map(), Map(), Map(), Set(), Set(), Set(), Seq(), None)
|
||||
}
|
||||
|
||||
case class InfiniteLoopException(data: Data, tx: Transaction) extends Exception
|
||||
|
|
|
@ -30,6 +30,7 @@ import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
|
|||
import fr.acinq.eclair.crypto.{Generators, LocalKeyManager, ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo}
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{ChannelReestablish, _}
|
||||
|
||||
|
@ -59,8 +60,8 @@ object Channel {
|
|||
// we won't exchange more than this many signatures when negotiating the closing fee
|
||||
val MAX_NEGOTIATION_ITERATIONS = 20
|
||||
|
||||
// this is defined in BOLT 11
|
||||
val MIN_CLTV_EXPIRY = 9L
|
||||
// this is defined in BOLT 7
|
||||
val MIN_CLTV_EXPIRY = 7L
|
||||
val MAX_CLTV_EXPIRY = 7 * 144L // one week
|
||||
|
||||
case object TickRefreshChannelUpdate
|
||||
|
@ -613,6 +614,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
case u: UpdateFailHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id)
|
||||
case u: UpdateFailMalformedHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id)
|
||||
}
|
||||
// On Android we don't store htlc informations, because since wallet is spend only a revoked transaction is
|
||||
// always in our favor and we don't need to steal the htlcs
|
||||
context.system.eventStream.publish(ChannelSignatureSent(self, commitments1))
|
||||
handleCommandSuccess(sender, store(d.copy(commitments = commitments1))) sending commit
|
||||
case Failure(cause) => handleCommandError(cause, c)
|
||||
|
@ -745,7 +748,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
|
||||
case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) =>
|
||||
val networkFeeratePerKw = feeratesPerKw.block_1
|
||||
val networkFeeratePerKw = feeratesPerKw.blocks_2
|
||||
d.commitments.localParams.isFunder match {
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
|
||||
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
|
||||
|
@ -990,7 +993,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
handleLocalError(HtlcTimedout(d.channelId), d, Some(c))
|
||||
|
||||
case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) =>
|
||||
val networkFeeratePerKw = feerates.block_1
|
||||
val networkFeeratePerKw = feerates.blocks_2
|
||||
d.commitments.localParams.isFunder match {
|
||||
case true if Helpers.shouldUpdateFee(d.commitments.localCommit.spec.feeratePerKw, networkFeeratePerKw, nodeParams.updateFeeMinDiffRatio) =>
|
||||
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
|
||||
|
@ -1125,14 +1128,20 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
// when a remote or local commitment tx containing outgoing htlcs is published on the network,
|
||||
// we watch it in order to extract payment preimage if funds are pulled by the counterparty
|
||||
// we can then use these preimages to fulfill origin htlcs
|
||||
log.warning(s"processing BITCOIN_OUTPUT_SPENT with txid=${tx.txid} tx=$tx")
|
||||
log.info(s"processing BITCOIN_OUTPUT_SPENT with txid=${tx.txid} tx=$tx")
|
||||
val extracted = Closing.extractPreimages(d.commitments.localCommit, tx)
|
||||
extracted map { case (htlc, fulfill) =>
|
||||
val origin = d.commitments.originChannels(fulfill.id)
|
||||
log.warning(s"fulfilling htlc #${fulfill.id} paymentHash=${sha256(fulfill.paymentPreimage)} origin=$origin")
|
||||
relayer ! ForwardFulfill(fulfill, origin, htlc)
|
||||
}
|
||||
stay
|
||||
val revokedCommitPublished1 = d.revokedCommitPublished.map { rev =>
|
||||
val (rev1, tx_opt) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx)
|
||||
tx_opt.foreach(claimTx => blockchain ! PublishAsap(claimTx))
|
||||
tx_opt.foreach(claimTx => blockchain ! WatchSpent(self, tx, claimTx.txIn.head.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT))
|
||||
rev1
|
||||
}
|
||||
stay using store(d.copy(revokedCommitPublished = revokedCommitPublished1))
|
||||
|
||||
case Event(WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _), d: DATA_CLOSING) =>
|
||||
log.info(s"txid=${tx.txid} has reached mindepth, updating closing state")
|
||||
|
@ -1626,13 +1635,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
def doPublish(localCommitPublished: LocalCommitPublished) = {
|
||||
import localCommitPublished._
|
||||
|
||||
val publishQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ htlcSuccessTxs ++ htlcTimeoutTxs ++ claimHtlcDelayedTx
|
||||
val publishQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ htlcSuccessTxs ++ htlcTimeoutTxs ++ claimHtlcDelayedTxs
|
||||
publishIfNeeded(publishQueue, irrevocablySpent)
|
||||
|
||||
// we watch:
|
||||
// - the commitment tx itself, so that we can handle the case where we don't have any outputs
|
||||
// - 'final txes' that send funds to our wallet and that spend outputs that only us control
|
||||
val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ claimHtlcDelayedTx
|
||||
val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx ++ claimHtlcDelayedTxs
|
||||
watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent)
|
||||
|
||||
// we watch outputs of the commitment tx that both parties may spend
|
||||
|
@ -1705,7 +1714,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = {
|
||||
log.warning(s"funding tx spent in txid=${tx.txid}")
|
||||
|
||||
Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx) match {
|
||||
Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.channelsDb) match {
|
||||
case Some(revokedCommitPublished) =>
|
||||
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx")
|
||||
val exc = FundingTxSpent(d.channelId, tx)
|
||||
|
@ -1729,17 +1738,17 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
|||
def doPublish(revokedCommitPublished: RevokedCommitPublished) = {
|
||||
import revokedCommitPublished._
|
||||
|
||||
val publishQueue = claimMainOutputTx ++ mainPenaltyTx ++ claimHtlcTimeoutTxs ++ htlcTimeoutTxs ++ htlcPenaltyTxs
|
||||
val publishQueue = claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs
|
||||
publishIfNeeded(publishQueue, irrevocablySpent)
|
||||
|
||||
// we watch:
|
||||
// - the commitment tx itself, so that we can handle the case where we don't have any outputs
|
||||
// - 'final txes' that send funds to our wallet and that spend outputs that only us control
|
||||
val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx ++ htlcPenaltyTxs
|
||||
val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx
|
||||
watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent)
|
||||
|
||||
// we watch outputs of the commitment tx that both parties may spend
|
||||
val watchSpentQueue = mainPenaltyTx ++ claimHtlcTimeoutTxs ++ htlcTimeoutTxs
|
||||
val watchSpentQueue = mainPenaltyTx ++ htlcPenaltyTxs
|
||||
watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent)
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,6 @@ case class LocalChannelDown(channel: ActorRef, channelId: BinaryData, shortChann
|
|||
|
||||
case class ChannelStateChanged(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, previousState: State, currentState: State, currentData: Data) extends ChannelEvent
|
||||
|
||||
case class ChannelSignatureSent(channel: ActorRef, Commitments: Commitments) extends ChannelEvent
|
||||
case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class ChannelSignatureReceived(channel: ActorRef, Commitments: Commitments) extends ChannelEvent
|
||||
case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent
|
||||
|
|
|
@ -140,9 +140,9 @@ trait HasCommitments extends Data {
|
|||
|
||||
case class ClosingTxProposed(unsignedTx: Transaction, localClosingSigned: ClosingSigned)
|
||||
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTx: List[Transaction], irrevocablySpent: Map[OutPoint, BinaryData])
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, BinaryData])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, BinaryData])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], claimHtlcTimeoutTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], htlcPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, BinaryData])
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, BinaryData])
|
||||
|
||||
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data
|
||||
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data
|
||||
|
|
|
@ -326,7 +326,7 @@ object Commitments {
|
|||
throw FundeeCannotSendUpdateFee(commitments.channelId)
|
||||
}
|
||||
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2
|
||||
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) {
|
||||
throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import fr.acinq.bitcoin.Script._
|
|||
import fr.acinq.bitcoin.{OutPoint, _}
|
||||
import fr.acinq.eclair.blockchain.EclairWallet
|
||||
import fr.acinq.eclair.crypto.{Generators, KeyManager}
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.transactions.Scripts._
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
|
@ -60,7 +61,7 @@ object Helpers {
|
|||
if (nodeParams.chainHash != open.chainHash) throw InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)
|
||||
if (open.fundingSatoshis < Channel.MIN_FUNDING_SATOSHIS || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS)
|
||||
if (open.pushMsat > 1000 * open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis)
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
val localFeeratePerKw = Globals.feeratesPerKw.get.blocks_2
|
||||
if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)
|
||||
// only enforce dust limit check on mainnet
|
||||
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
|
||||
|
@ -121,7 +122,9 @@ object Helpers {
|
|||
import scala.concurrent.duration._
|
||||
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)
|
||||
val finalScriptPubKey = Base58Check.decode(finalAddress) match {
|
||||
case (Base58.Prefix.PubkeyAddress, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
|
||||
case (Base58.Prefix.PubkeyAddressTestnet, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
|
||||
case (Base58.Prefix.ScriptAddress, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil)
|
||||
case (Base58.Prefix.ScriptAddressTestnet, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil)
|
||||
}
|
||||
finalScriptPubKey
|
||||
|
@ -295,7 +298,7 @@ object Helpers {
|
|||
|
||||
// all htlc output to us are delayed, so we need to claim them as soon as the delay is over
|
||||
val htlcDelayedTxes = htlcTxes.flatMap {
|
||||
txinfo: TransactionWithInputInfo => generateTx("claim-delayed-output")(Try {
|
||||
txinfo: TransactionWithInputInfo => generateTx("claim-htlc-delayed")(Try {
|
||||
val claimDelayed = Transactions.makeClaimDelayedOutputTx(
|
||||
txinfo.tx,
|
||||
Satoshi(localParams.dustLimitSatoshis),
|
||||
|
@ -308,16 +311,12 @@ object Helpers {
|
|||
})
|
||||
}
|
||||
|
||||
// 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),
|
||||
claimHtlcDelayedTxs = htlcDelayedTxes.map(_.tx),
|
||||
irrevocablySpent = Map.empty)
|
||||
}
|
||||
|
||||
|
@ -336,15 +335,13 @@ object Helpers {
|
|||
val (remoteCommitTx, _, _) = Commitments.makeRemoteTxs(keyManager, 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 localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
|
||||
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
|
||||
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint)
|
||||
val localPerCommitmentPoint = keyManager.commitmentPoint(localParams.channelKeyPath, commitments.localCommit.index.toInt)
|
||||
val localRevocationPubKey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
|
||||
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint)
|
||||
|
||||
// we need to use a rather high fee for htlc-claim because we compete with the counterparty
|
||||
val feeratePerKwHtlc = Globals.feeratesPerKw.get.block_1
|
||||
val feeratePerKwHtlc = Globals.feeratesPerKw.get.blocks_2
|
||||
|
||||
// those are the preimages to existing received htlcs
|
||||
val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }
|
||||
|
@ -369,9 +366,6 @@ object Helpers {
|
|||
})
|
||||
}.toSeq.flatten
|
||||
|
||||
// OPTIONAL: let's check transactions are actually spendable
|
||||
//require(txes.forall(Transactions.checkSpendable(_).isSuccess), "the tx we produced are not spendable!")
|
||||
|
||||
claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx).copy(
|
||||
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
|
||||
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx }
|
||||
|
@ -420,28 +414,29 @@ object Helpers {
|
|||
*
|
||||
* @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment
|
||||
*/
|
||||
def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = {
|
||||
def claimRevokedRemoteCommitTxOutputs(keyManager: KeyManager, commitments: Commitments, tx: Transaction, db: ChannelsDb)(implicit log: LoggingAdapter): 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, keyManager.paymentPoint(localParams.channelKeyPath).publicKey)
|
||||
require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long")
|
||||
log.warning(s"counterparty has published revoked commit txnumber=$txnumber")
|
||||
log.warning(s"a revoked commit has been published with txnumber=$txnumber")
|
||||
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
|
||||
remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txnumber)
|
||||
.map(d => Scalar(d))
|
||||
.map { remotePerCommitmentSecret =>
|
||||
val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint
|
||||
|
||||
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
|
||||
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentSecret.toPoint)
|
||||
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
|
||||
val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
|
||||
val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
|
||||
val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, 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
|
||||
val feeratePerKwPenalty = Globals.feeratesPerKw.get.blocks_2
|
||||
|
||||
// first we will claim our main output right away
|
||||
val mainTx = generateTx("claim-p2wpkh-output")(Try {
|
||||
|
@ -457,24 +452,94 @@ object Helpers {
|
|||
Transactions.addSigs(txinfo, sig)
|
||||
})
|
||||
|
||||
// TODO: we don't claim htlcs outputs yet for revoked transactions
|
||||
// we retrieve the informations needed to rebuild htlc scripts
|
||||
val htlcInfos = db.listHtlcHtlcInfos(commitments.channelId, txnumber)
|
||||
log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txnumber")
|
||||
val htlcsRedeemScripts = (
|
||||
htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry) } ++
|
||||
htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash)) }
|
||||
)
|
||||
.map(redeemScript => (Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)))
|
||||
.toMap
|
||||
|
||||
// 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!")
|
||||
// and finally we steal the htlc outputs
|
||||
val htlcPenaltyTxs = tx.txOut.collect { case txOut if htlcsRedeemScripts.contains(txOut.publicKeyScript) =>
|
||||
val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript)
|
||||
generateTx("htlc-penalty")(Try {
|
||||
val txinfo = Transactions.makeHtlcPenaltyTx(tx, htlcRedeemScript, Satoshi(localParams.dustLimitSatoshis), localParams.defaultFinalScriptPubKey, feeratePerKwPenalty)
|
||||
val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret)
|
||||
Transactions.addSigs(txinfo, sig, remoteRevocationPubkey)
|
||||
})
|
||||
}.toList.flatten
|
||||
|
||||
RevokedCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainOutputTx = mainTx.map(_.tx),
|
||||
mainPenaltyTx = mainPenaltyTx.map(_.tx),
|
||||
claimHtlcTimeoutTxs = Nil,
|
||||
htlcTimeoutTxs = Nil,
|
||||
htlcPenaltyTxs = Nil,
|
||||
htlcPenaltyTxs = htlcPenaltyTxs.map(_.tx),
|
||||
claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx
|
||||
irrevocablySpent = Map.empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key.
|
||||
*
|
||||
* In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment:
|
||||
* - by spending the corresponding output of the commitment tx, using [[HtlcPenaltyTx]] that we generate as soon as we detect that a revoked commit
|
||||
* as been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty.
|
||||
* - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txes is protected by
|
||||
* an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand",
|
||||
* this is the purpose of this method.
|
||||
*
|
||||
* @param keyManager
|
||||
* @param commitments
|
||||
* @param revokedCommitPublished
|
||||
* @param htlcTx
|
||||
* @return
|
||||
*/
|
||||
def claimRevokedHtlcTxOutputs(keyManager: KeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction)(implicit log: LoggingAdapter): (RevokedCommitPublished, Option[Transaction]) = {
|
||||
if (htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) &&
|
||||
!(revokedCommitPublished.claimMainOutputTx ++ revokedCommitPublished.mainPenaltyTx ++ revokedCommitPublished.htlcPenaltyTxs).map(_.txid).toSet.contains(htlcTx.txid)) {
|
||||
log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}")
|
||||
// Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key
|
||||
import commitments._
|
||||
val tx = revokedCommitPublished.commitTx
|
||||
val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn(0).sequence, tx.lockTime)
|
||||
// this tx has been published by remote, so we need to invert local/remote params
|
||||
val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(localParams.channelKeyPath).publicKey)
|
||||
// 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))
|
||||
.flatMap { remotePerCommitmentSecret =>
|
||||
val remotePerCommitmentPoint = remotePerCommitmentSecret.toPoint
|
||||
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
|
||||
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(localParams.channelKeyPath).publicKey, remotePerCommitmentPoint)
|
||||
|
||||
// we need to use a high fee here for punishment txes because after a delay they can be spent by the counterparty
|
||||
val feeratePerKwPenalty = Globals.feeratesPerKw.get.block_1
|
||||
|
||||
generateTx("claim-htlc-delayed-penalty")(Try {
|
||||
val txinfo = Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, Satoshi(localParams.dustLimitSatoshis), remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty)
|
||||
val sig = keyManager.sign(txinfo, keyManager.revocationPoint(localParams.channelKeyPath), remotePerCommitmentSecret)
|
||||
val signedTx = Transactions.addSigs(txinfo, sig)
|
||||
// we need to make sure that the tx is indeed valid
|
||||
Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
signedTx
|
||||
})
|
||||
} match {
|
||||
case Some(tx) =>
|
||||
val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs :+ tx.tx)
|
||||
(revokedCommitPublished1, Some(tx.tx))
|
||||
case None =>
|
||||
(revokedCommitPublished, None)
|
||||
}
|
||||
} else {
|
||||
(revokedCommitPublished, None)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In CLOSING state, any time we see a new transaction, we try to extract a preimage from it in order to fulfill the
|
||||
* corresponding incoming htlc in an upstream channel.
|
||||
|
@ -569,6 +634,7 @@ object Helpers {
|
|||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param localCommitPublished
|
||||
* @param tx a transaction that has been irrevocably confirmed
|
||||
* @return
|
||||
*/
|
||||
def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction) = {
|
||||
|
@ -581,7 +647,7 @@ object Helpers {
|
|||
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)
|
||||
val is3rdStageDelayedTx = localCommitPublished.claimHtlcDelayedTxs.map(_.txid).contains(tx.txid)
|
||||
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
|
@ -597,6 +663,7 @@ object Helpers {
|
|||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param remoteCommitPublished
|
||||
* @param tx a transaction that has been irrevocably confirmed
|
||||
* @return
|
||||
*/
|
||||
def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction) = {
|
||||
|
@ -622,6 +689,7 @@ object Helpers {
|
|||
* want to wait forever before declaring that the channel is CLOSED.
|
||||
*
|
||||
* @param revokedCommitPublished
|
||||
* @param tx a transaction that has been irrevocably confirmed
|
||||
* @return
|
||||
*/
|
||||
def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction) = {
|
||||
|
@ -632,8 +700,10 @@ object Helpers {
|
|||
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
|
||||
// TODO: we don't claim htlcs outputs yet for revoked transactions
|
||||
isCommitTx || spendsTheCommitTx
|
||||
// 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 = revokedCommitPublished.claimHtlcDelayedPenaltyTxs.map(_.txid).contains(tx.txid)
|
||||
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
revokedCommitPublished.copy(irrevocablySpent = revokedCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => (o -> tx.txid)).toMap)
|
||||
|
@ -648,13 +718,13 @@ object Helpers {
|
|||
* @return
|
||||
*/
|
||||
def isLocalCommitDone(localCommitPublished: LocalCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
// is the commitment tx buried? (we need to check this because we may not have any outputs)
|
||||
val isCommitTxConfirmed = localCommitPublished.irrevocablySpent.values.toSet.contains(localCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx? we just subtract all known spent outputs from the ones we control
|
||||
val commitOutputsSpendableByUs = (localCommitPublished.claimMainDelayedOutputTx.toSeq ++ localCommitPublished.htlcSuccessTxs ++ localCommitPublished.htlcTimeoutTxs)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- localCommitPublished.irrevocablySpent.keys
|
||||
// which htlc delayed txes can we expect to be confirmed?
|
||||
val unconfirmedHtlcDelayedTxes = localCommitPublished.claimHtlcDelayedTx
|
||||
val unconfirmedHtlcDelayedTxes = localCommitPublished.claimHtlcDelayedTxs
|
||||
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- localCommitPublished.irrevocablySpent.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.irrevocablySpent.values.toSet.contains(tx.txid)) // has the tx already been confirmed?
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxes.isEmpty
|
||||
|
@ -668,7 +738,7 @@ object Helpers {
|
|||
* @return
|
||||
*/
|
||||
def isRemoteCommitDone(remoteCommitPublished: RemoteCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
// is the commitment tx buried? (we need to check this because we may not have any outputs)
|
||||
val isCommitTxConfirmed = remoteCommitPublished.irrevocablySpent.values.toSet.contains(remoteCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx?
|
||||
val commitOutputsSpendableByUs = (remoteCommitPublished.claimMainOutputTx.toSeq ++ remoteCommitPublished.claimHtlcSuccessTxs ++ remoteCommitPublished.claimHtlcTimeoutTxs)
|
||||
|
@ -684,13 +754,16 @@ object Helpers {
|
|||
* @return
|
||||
*/
|
||||
def isRevokedCommitDone(revokedCommitPublished: RevokedCommitPublished) = {
|
||||
// is the commitment tx buried? (we need to check this because we may not have nay outputs)
|
||||
// is the commitment tx buried? (we need to check this because we may not have any outputs)
|
||||
val isCommitTxConfirmed = revokedCommitPublished.irrevocablySpent.values.toSet.contains(revokedCommitPublished.commitTx.txid)
|
||||
// are there remaining spendable outputs from the commitment tx?
|
||||
val commitOutputsSpendableByUs = (revokedCommitPublished.claimMainOutputTx.toSeq ++ revokedCommitPublished.mainPenaltyTx)
|
||||
val commitOutputsSpendableByUs = (revokedCommitPublished.claimMainOutputTx.toSeq ++ revokedCommitPublished.mainPenaltyTx ++ revokedCommitPublished.htlcPenaltyTxs)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- revokedCommitPublished.irrevocablySpent.keys
|
||||
// TODO: we don't claim htlcs outputs yet for revoked transactions
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty
|
||||
// which htlc delayed txes can we expect to be confirmed?
|
||||
val unconfirmedHtlcDelayedTxes = revokedCommitPublished.claimHtlcDelayedPenaltyTxs
|
||||
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- revokedCommitPublished.irrevocablySpent.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 => revokedCommitPublished.irrevocablySpent.values.toSet.contains(tx.txid)) // has the tx already been confirmed?
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxes.isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,19 +19,26 @@ package fr.acinq.eclair.crypto
|
|||
import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, DeterministicWallet}
|
||||
import fr.acinq.eclair.ShortChannelId
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo
|
||||
|
||||
object LocalKeyManager {
|
||||
val channelKeyBasePath = DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil
|
||||
def channelKeyBasePath(chainHash: BinaryData) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil
|
||||
case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil
|
||||
}
|
||||
|
||||
|
||||
// WARNING: if you change this path, you will change your node id even if the seed remains the same!!!
|
||||
// Note that the node path and the above channel path are on different branches so even if the
|
||||
// node key is compromised there is no way to retrieve the wallet keys
|
||||
val nodeKeyBasePath = DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil
|
||||
def nodeKeyBasePath(chainHash: BinaryData) = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil
|
||||
case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(0) :: Nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,10 +47,10 @@ object LocalKeyManager {
|
|||
*
|
||||
* @param seed seed from which keys will be derived
|
||||
*/
|
||||
class LocalKeyManager(seed: BinaryData) extends KeyManager {
|
||||
class LocalKeyManager(seed: BinaryData, chainHash: BinaryData) extends KeyManager {
|
||||
private val master = DeterministicWallet.generate(seed)
|
||||
|
||||
override val nodeKey = DeterministicWallet.derivePrivateKey(master, LocalKeyManager.nodeKeyBasePath)
|
||||
override val nodeKey = DeterministicWallet.derivePrivateKey(master, LocalKeyManager.nodeKeyBasePath(chainHash))
|
||||
override val nodeId = nodeKey.publicKey
|
||||
|
||||
private val privateKeys: LoadingCache[KeyPath, ExtendedPrivateKey] = CacheBuilder.newBuilder()
|
||||
|
@ -58,7 +65,7 @@ class LocalKeyManager(seed: BinaryData) extends KeyManager {
|
|||
override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath))
|
||||
})
|
||||
|
||||
private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalKeyManager.channelKeyBasePath ++ channelKeyPath.path) :+ index
|
||||
private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalKeyManager.channelKeyBasePath(chainHash) ++ channelKeyPath.path) :+ index
|
||||
|
||||
private def fundingPrivateKey(channelKeyPath: DeterministicWallet.KeyPath) = privateKeys.get(internalKeyPath(channelKeyPath, hardened(0)))
|
||||
|
||||
|
|
|
@ -25,12 +25,14 @@ import fr.acinq.bitcoin.Crypto.PublicKey
|
|||
import fr.acinq.bitcoin.{BinaryData, Protocol}
|
||||
import fr.acinq.eclair.crypto.Noise._
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement}
|
||||
import scodec.Attempt.Successful
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec, DecodeResult}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable.Queue
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* see BOLT #8
|
||||
|
@ -71,12 +73,14 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[BinaryData], co
|
|||
|
||||
def sendToListener(listener: ActorRef, plaintextMessages: Seq[BinaryData]): Map[T, Int] = {
|
||||
var m: Map[T, Int] = Map()
|
||||
plaintextMessages.foreach(plaintext => codec.decode(BitVector(plaintext.data)) match {
|
||||
case Attempt.Successful(DecodeResult(message, _)) =>
|
||||
plaintextMessages.foreach(plaintext => Try(codec.decode(BitVector(plaintext.data))) match {
|
||||
case Success(Attempt.Successful(DecodeResult(message, _))) =>
|
||||
listener ! message
|
||||
m += (message -> (m.getOrElse(message, 0) + 1))
|
||||
case Attempt.Failure(err) =>
|
||||
case Success(Attempt.Failure(err)) =>
|
||||
log.error(s"cannot deserialize $plaintext: $err")
|
||||
case Failure(t) =>
|
||||
log.error(s"cannot deserialize $plaintext: ${t.getMessage}")
|
||||
})
|
||||
m
|
||||
}
|
||||
|
|
|
@ -27,4 +27,8 @@ trait ChannelsDb {
|
|||
|
||||
def listChannels(): Seq[HasCommitments]
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: BinaryData, commitmentNumber: Long, paymentHash: BinaryData, cltvExpiry: Long)
|
||||
|
||||
def listHtlcHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)]
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,9 @@ import fr.acinq.bitcoin.BinaryData
|
|||
import fr.acinq.eclair.channel.HasCommitments
|
||||
import fr.acinq.eclair.db.ChannelsDb
|
||||
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
|
||||
import scodec.bits.BitVector
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
|
||||
class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
||||
|
||||
|
@ -32,7 +35,10 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
|||
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed
|
||||
statement.execute("PRAGMA foreign_keys = ON")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)")
|
||||
}
|
||||
|
||||
override def addOrUpdateChannel(state: HasCommitments): Unit = {
|
||||
|
@ -56,6 +62,11 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
|||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement("DELETE FROM htlc_infos WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")) { statement =>
|
||||
statement.setBytes(1, channelId)
|
||||
statement.executeUpdate()
|
||||
|
@ -68,4 +79,27 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb {
|
|||
codecSequence(rs, stateDataCodec)
|
||||
}
|
||||
}
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: BinaryData, commitmentNumber: Long, paymentHash: BinaryData, cltvExpiry: Long): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, commitmentNumber)
|
||||
statement.setBytes(3, paymentHash)
|
||||
statement.setLong(4, cltvExpiry)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
def listHtlcHtlcInfos(channelId: BinaryData, commitmentNumber: Long): Seq[(BinaryData, Long)] = {
|
||||
using(sqlite.prepareStatement("SELECT payment_hash, cltv_expiry FROM htlc_infos WHERE channel_id=? AND commitment_number=?")) { statement =>
|
||||
statement.setBytes(1, channelId)
|
||||
statement.setLong(2, commitmentNumber)
|
||||
val rs = statement.executeQuery
|
||||
var q: Queue[(BinaryData, Long)] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ (BinaryData(rs.getBytes("payment_hash")), rs.getLong("cltv_expiry"))
|
||||
}
|
||||
q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor
|
|||
log.info(s"requesting a new channel to $remoteNodeId with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt}")
|
||||
val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis.toLong, origin_opt = Some(sender))
|
||||
val temporaryChannelId = randomBytes(32)
|
||||
val channelFeeratePerKw = Globals.feeratesPerKw.get.block_1
|
||||
val channelFeeratePerKw = Globals.feeratesPerKw.get.blocks_2
|
||||
val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(Globals.feeratesPerKw.get.blocks_6)
|
||||
channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis.amount, c.pushMsat.amount, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, transport, remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags))
|
||||
stay using d.copy(channels = channels + (TemporaryChannelId(temporaryChannelId) -> channel))
|
||||
|
|
|
@ -60,14 +60,7 @@ package object eclair {
|
|||
def feerateByte2Kw(feeratePerByte: Long): Long = feeratePerByte * 1024 / 4
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param address bitcoin Base58 address
|
||||
* @return true if the address is a segwit address i.e. a p2sh-of-p2wpkh address.
|
||||
* We approximate this be returning true if the address is a p2sh address, there is no
|
||||
* way to tell what the script is.
|
||||
*/
|
||||
def isSegwitAddress(address: String) : Boolean = address.startsWith("2") || address.startsWith("3")
|
||||
def isPay2PubkeyHash(address: String) : Boolean = address.startsWith("1") || address.startsWith("m") || address.startsWith("n")
|
||||
|
||||
/**
|
||||
* Tests whether the binary data is composed solely of printable ASCII characters (see BOLT 1)
|
||||
|
|
|
@ -225,8 +225,8 @@ object PaymentRequest {
|
|||
}
|
||||
|
||||
def fromBech32Address(address: String): FallbackAddressTag = {
|
||||
val (prefix, hash) = Bech32.decodeWitnessAddress(address)
|
||||
FallbackAddressTag(prefix, hash)
|
||||
val (_, version, hash) = Bech32.decodeWitnessAddress(address)
|
||||
FallbackAddressTag(version, hash)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -263,4 +263,11 @@ object Scripts {
|
|||
def witnessClaimHtlcTimeoutFromCommitTx(localSig: BinaryData, htlcReceivedScript: BinaryData) =
|
||||
ScriptWitness(localSig :: BinaryData.empty :: htlcReceivedScript :: Nil)
|
||||
|
||||
/**
|
||||
* This witness script spends (steals) a [[htlcOffered]] or [[htlcReceived]] output using a revocation key as a punishment
|
||||
* for having published a revoked transaction
|
||||
*/
|
||||
def witnessHtlcWithRevocationSig(revocationSig: BinaryData, revocationPubkey: PublicKey, htlcScript: BinaryData) =
|
||||
ScriptWitness(revocationSig :: revocationPubkey.toBin :: htlcScript :: Nil)
|
||||
|
||||
}
|
|
@ -50,6 +50,7 @@ object Transactions {
|
|||
case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class ClaimDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class ClaimDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo
|
||||
|
@ -77,10 +78,10 @@ object Transactions {
|
|||
* - [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]]
|
||||
* - [[MainPenaltyTx]] spends remote main output using the per-commitment secret
|
||||
* - [[HtlcSuccessTx]] spends htlc-sent outputs of [[CommitTx]] for which they have the preimage (published by remote)
|
||||
* - [[HtlcPenaltyTx]] spends [[HtlcSuccessTx]] using the per-commitment secret
|
||||
* - [[ClaimHtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout
|
||||
* - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by local or remote)
|
||||
* - [[HtlcPenaltyTx]] spends [[HtlcTimeoutTx]] using the per-commitment secret
|
||||
* - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local)
|
||||
* - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by remote)
|
||||
* - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local)
|
||||
* - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local)
|
||||
*/
|
||||
|
||||
val commitWeight = 724
|
||||
|
@ -91,6 +92,7 @@ object Transactions {
|
|||
val claimHtlcSuccessWeight = 570
|
||||
val claimHtlcTimeoutWeight = 544
|
||||
val mainPenaltyWeight = 483
|
||||
val htlcPenaltyWeight = 577 // based on spending an HTLC-Success output (would be 571 with HTLC-Timeout)
|
||||
|
||||
def weight2fee(feeratePerKw: Long, weight: Int) = Satoshi((feeratePerKw * weight) / 1000)
|
||||
|
||||
|
@ -212,7 +214,7 @@ object Transactions {
|
|||
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
|
||||
HtlcTimeoutTx(input, Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0) :: Nil,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil,
|
||||
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
|
||||
lockTime = htlc.expiry))
|
||||
}
|
||||
|
@ -229,7 +231,7 @@ object Transactions {
|
|||
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
|
||||
HtlcSuccessTx(input, Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0) :: Nil,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0x00000000L) :: Nil,
|
||||
txOut = TxOut(amount, pay2wsh(toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))) :: Nil,
|
||||
lockTime = 0), htlc.paymentHash)
|
||||
}
|
||||
|
@ -310,6 +312,23 @@ object Transactions {
|
|||
lockTime = 0))
|
||||
}
|
||||
|
||||
def makeClaimDelayedOutputPenaltyTx(delayedOutputTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: Int, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: BinaryData, feeratePerKw: Long): ClaimDelayedOutputPenaltyTx = {
|
||||
val fee = weight2fee(feeratePerKw, claimHtlcDelayedWeight)
|
||||
val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)
|
||||
val pubkeyScript = write(pay2wsh(redeemScript))
|
||||
val outputIndex = findPubKeyScriptIndex(delayedOutputTx, pubkeyScript)
|
||||
val input = InputInfo(OutPoint(delayedOutputTx, outputIndex), delayedOutputTx.txOut(outputIndex), write(redeemScript))
|
||||
val amount = input.txOut.amount - fee
|
||||
if (amount < localDustLimit) {
|
||||
throw AmountBelowDustLimit
|
||||
}
|
||||
ClaimDelayedOutputPenaltyTx(input, Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0xffffffffL) :: Nil,
|
||||
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
|
||||
lockTime = 0))
|
||||
}
|
||||
|
||||
def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: BinaryData, toRemoteDelay: Int, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: Long): MainPenaltyTx = {
|
||||
val fee = weight2fee(feeratePerKw, mainPenaltyWeight)
|
||||
val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey)
|
||||
|
@ -327,7 +346,24 @@ object Transactions {
|
|||
lockTime = 0))
|
||||
}
|
||||
|
||||
def makeHtlcPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi): HtlcPenaltyTx = ???
|
||||
/**
|
||||
* We already have the redeemScript, no need to build it
|
||||
*/
|
||||
def makeHtlcPenaltyTx(commitTx: Transaction, redeemScript: BinaryData, localDustLimit: Satoshi, localFinalScriptPubKey: BinaryData, feeratePerKw: Long): HtlcPenaltyTx = {
|
||||
val fee = weight2fee(feeratePerKw, htlcPenaltyWeight)
|
||||
val pubkeyScript = write(pay2wsh(redeemScript))
|
||||
val outputIndex = findPubKeyScriptIndex(commitTx, pubkeyScript)
|
||||
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), redeemScript)
|
||||
val amount = input.txOut.amount - fee
|
||||
if (amount < localDustLimit) {
|
||||
throw AmountBelowDustLimit
|
||||
}
|
||||
HtlcPenaltyTx(input, Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(input.outPoint, Array.emptyByteArray, 0xffffffffL) :: Nil,
|
||||
txOut = TxOut(amount, localFinalScriptPubKey) :: Nil,
|
||||
lockTime = 0))
|
||||
}
|
||||
|
||||
def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: BinaryData, remoteScriptPubKey: BinaryData, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = {
|
||||
require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs")
|
||||
|
@ -364,14 +400,6 @@ object Transactions {
|
|||
Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, key)
|
||||
}
|
||||
|
||||
// when the amount is not specified, we used the legacy (pre-segwit) signature scheme
|
||||
// this is only used to spend the to-remote output of a commit tx, which is the only non-segwit output
|
||||
// that we use
|
||||
// TODO: change this if the decide to use P2WPKH in the to-remote output
|
||||
def sign(tx: Transaction, inputIndex: Int, redeemScript: BinaryData, key: PrivateKey): BinaryData = {
|
||||
Transaction.signInput(tx, inputIndex, redeemScript, SIGHASH_ALL, Satoshi(0), SIGVERSION_BASE, key)
|
||||
}
|
||||
|
||||
def sign(txinfo: TransactionWithInputInfo, key: PrivateKey): BinaryData = {
|
||||
require(txinfo.tx.txIn.lengthCompare(1) == 0, "only one input allowed")
|
||||
sign(txinfo.tx, inputIndex = 0, txinfo.input.redeemScript, txinfo.input.txOut.amount, key)
|
||||
|
@ -382,9 +410,14 @@ object Transactions {
|
|||
commitTx.copy(tx = commitTx.tx.updateWitness(0, witness))
|
||||
}
|
||||
|
||||
def addSigs(claimMainDelayedRevokedTx: MainPenaltyTx, revocationSig: BinaryData): MainPenaltyTx = {
|
||||
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimMainDelayedRevokedTx.input.redeemScript)
|
||||
claimMainDelayedRevokedTx.copy(tx = claimMainDelayedRevokedTx.tx.updateWitness(0, witness))
|
||||
def addSigs(mainPenaltyTx: MainPenaltyTx, revocationSig: BinaryData): MainPenaltyTx = {
|
||||
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, mainPenaltyTx.input.redeemScript)
|
||||
mainPenaltyTx.copy(tx = mainPenaltyTx.tx.updateWitness(0, witness))
|
||||
}
|
||||
|
||||
def addSigs(htlcPenaltyTx: HtlcPenaltyTx, revocationSig: BinaryData, revocationPubkey: PublicKey): HtlcPenaltyTx = {
|
||||
val witness = Scripts.witnessHtlcWithRevocationSig(revocationSig, revocationPubkey, htlcPenaltyTx.input.redeemScript)
|
||||
htlcPenaltyTx.copy(tx = htlcPenaltyTx.tx.updateWitness(0, witness))
|
||||
}
|
||||
|
||||
def addSigs(htlcSuccessTx: HtlcSuccessTx, localSig: BinaryData, remoteSig: BinaryData, paymentPreimage: BinaryData): HtlcSuccessTx = {
|
||||
|
@ -417,6 +450,11 @@ object Transactions {
|
|||
claimHtlcDelayed.copy(tx = claimHtlcDelayed.tx.updateWitness(0, witness))
|
||||
}
|
||||
|
||||
def addSigs(claimHtlcDelayedPenalty: ClaimDelayedOutputPenaltyTx, revocationSig: BinaryData): ClaimDelayedOutputPenaltyTx = {
|
||||
val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript)
|
||||
claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness))
|
||||
}
|
||||
|
||||
def addSigs(closingTx: ClosingTx, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, localSig: BinaryData, remoteSig: BinaryData): ClosingTx = {
|
||||
val witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey, remoteFundingPubkey)
|
||||
closingTx.copy(tx = closingTx.tx.updateWitness(0, witness))
|
||||
|
|
|
@ -217,9 +217,8 @@ object ChannelCodecs extends Logging {
|
|||
("commitTx" | txCodec) ::
|
||||
("claimMainOutputTx" | optional(bool, txCodec)) ::
|
||||
("mainPenaltyTx" | optional(bool, txCodec)) ::
|
||||
("claimHtlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcTimeoutTxs" | listOfN(uint16, txCodec)) ::
|
||||
("htlcPenaltyTxs" | listOfN(uint16, txCodec)) ::
|
||||
("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) ::
|
||||
("spent" | spentMapCodec)).as[RevokedCommitPublished]
|
||||
|
||||
val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = (
|
||||
|
|
|
@ -7,4 +7,5 @@ rpcpassword=bar
|
|||
txindex=1
|
||||
zmqpubrawblock=tcp://127.0.0.1:28334
|
||||
zmqpubrawtx=tcp://127.0.0.1:28334
|
||||
rpcworkqueue=64
|
||||
rpcworkqueue=64
|
||||
addresstype=p2sh-segwit
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
</appender-->
|
||||
|
||||
<!--logger name="fr.acinq.eclair.channel" level="DEBUG"/-->
|
||||
<logger name="fr.acinq.eclair.router" level="WARN"/>
|
||||
|
||||
<root level="INFO">
|
||||
<!--appender-ref ref="FILE"/>
|
||||
|
|
|
@ -39,7 +39,7 @@ object TestConstants {
|
|||
|
||||
object Alice {
|
||||
val seed = BinaryData("01" * 32)
|
||||
val keyManager = new LocalKeyManager(seed)
|
||||
val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash)
|
||||
|
||||
def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
|
@ -70,7 +70,6 @@ object TestConstants {
|
|||
pendingRelayDb = new SqlitePendingRelayDb(sqlite),
|
||||
paymentsDb = new SqlitePaymentsDb(sqlite),
|
||||
routerBroadcastInterval = 60 seconds,
|
||||
routerValidateInterval = 2 seconds,
|
||||
pingInterval = 30 seconds,
|
||||
maxFeerateMismatch = 1.5,
|
||||
updateFeeMinDiffRatio = 0.1,
|
||||
|
@ -94,7 +93,7 @@ object TestConstants {
|
|||
|
||||
object Bob {
|
||||
val seed = BinaryData("02" * 32)
|
||||
val keyManager = new LocalKeyManager(seed)
|
||||
val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash)
|
||||
|
||||
def sqlite = DriverManager.getConnection("jdbc:sqlite::memory:")
|
||||
|
||||
|
@ -124,7 +123,6 @@ object TestConstants {
|
|||
pendingRelayDb = new SqlitePendingRelayDb(sqlite),
|
||||
paymentsDb = new SqlitePaymentsDb(sqlite),
|
||||
routerBroadcastInterval = 60 seconds,
|
||||
routerValidateInterval = 2 seconds,
|
||||
pingInterval = 30 seconds,
|
||||
maxFeerateMismatch = 1.0,
|
||||
updateFeeMinDiffRatio = 0.1,
|
||||
|
|
|
@ -46,7 +46,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
|
|||
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 = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
|
@ -79,9 +79,6 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with FunSuiteLi
|
|||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
bitcoind.exitValue()
|
||||
// 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") {
|
||||
|
|
|
@ -47,7 +47,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
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 = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.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"))
|
||||
|
|
|
@ -38,10 +38,10 @@ class ElectrumWalletBasicSpec extends FunSuite {
|
|||
val minimumFee = Satoshi(2000)
|
||||
|
||||
val master = DeterministicWallet.generate(BinaryData("01" * 32))
|
||||
val accountMaster = accountKey(master)
|
||||
val accountMaster = accountKey(master, Block.RegtestGenesisBlock.hash)
|
||||
val accountIndex = 0
|
||||
|
||||
val changeMaster = changeKey(master)
|
||||
val changeMaster = changeKey(master, Block.RegtestGenesisBlock.hash)
|
||||
val changeIndex = 0
|
||||
|
||||
val firstAccountKeys = (0 until 10).map(i => derivePrivateKey(accountMaster, i)).toVector
|
||||
|
@ -78,7 +78,7 @@ class ElectrumWalletBasicSpec extends FunSuite {
|
|||
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")
|
||||
assert(segwitAddress(priv, Block.RegtestGenesisBlock.hash) == "2MscvqgGXMTYJNAY3owdUtgWJaxPUjH38Cx")
|
||||
}
|
||||
|
||||
test("implement BIP49") {
|
||||
|
@ -86,9 +86,9 @@ class ElectrumWalletBasicSpec extends FunSuite {
|
|||
val seed = MnemonicCode.toSeed(mnemonics, "")
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
|
||||
val accountMaster = accountKey(master)
|
||||
val accountMaster = accountKey(master, Block.RegtestGenesisBlock.hash)
|
||||
val firstKey = derivePrivateKey(accountMaster, 0)
|
||||
assert(segwitAddress(firstKey) === "2MxJejujQJRRJdbfTKNQQ94YCnxJwRaE7yo")
|
||||
assert(segwitAddress(firstKey, Block.RegtestGenesisBlock.hash) === "2MxJejujQJRRJdbfTKNQQ94YCnxJwRaE7yo")
|
||||
}
|
||||
|
||||
test("complete transactions (enough funds)") {
|
||||
|
|
|
@ -1541,7 +1541,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val event = CurrentFeerates(FeeratesPerKw.single(20000))
|
||||
sender.send(alice, event)
|
||||
alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.block_1))
|
||||
alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1736,18 +1736,25 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
alice2bob.expectMsgType[Error]
|
||||
|
||||
val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val penaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
// val htlcPenaltyTxs = for (i <- 0 until 4) yield alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx))
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx))
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty
|
||||
// htlcPenaltyTxs.foreach(htlcPenaltyTx => assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT))
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
Transaction.correctlySpends(penaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
// htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
|
||||
|
||||
// two main outputs are 760 000 and 200 000
|
||||
assert(mainTx.txOut(0).amount == Satoshi(741510))
|
||||
assert(penaltyTx.txOut(0).amount == Satoshi(195170))
|
||||
assert(mainPenaltyTx.txOut(0).amount == Satoshi(195170))
|
||||
// assert(htlcPenaltyTxs(0).txOut(0).amount == Satoshi(4230))
|
||||
// assert(htlcPenaltyTxs(1).txOut(0).amount == Satoshi(4230))
|
||||
// assert(htlcPenaltyTxs(2).txOut(0).amount == Satoshi(4230))
|
||||
// assert(htlcPenaltyTxs(3).txOut(0).amount == Satoshi(4230))
|
||||
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
|
||||
|
@ -1819,7 +1826,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
assert(localCommitPublished.commitTx == aliceCommitTx)
|
||||
assert(localCommitPublished.htlcSuccessTxs.size == 1)
|
||||
assert(localCommitPublished.htlcTimeoutTxs.size == 2)
|
||||
assert(localCommitPublished.claimHtlcDelayedTx.size == 3)
|
||||
assert(localCommitPublished.claimHtlcDelayedTxs.size == 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1885,10 +1892,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
import initialState.commitments.localParams
|
||||
import initialState.commitments.remoteParams
|
||||
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
|
||||
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Bob.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
|
||||
// actual test starts here
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = annSigsA.shortChannelId, buried = true, channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate))
|
||||
awaitCond({
|
||||
val normal = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement == Some(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId
|
||||
})
|
||||
assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -581,7 +581,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN]
|
||||
val event = CurrentFeerates(FeeratesPerKw.single(20000))
|
||||
sender.send(alice, event)
|
||||
alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.block_1))
|
||||
alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, event.feeratesPerKw.blocks_2))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,18 +722,26 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
alice2bob.expectMsgType[Error]
|
||||
|
||||
val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val penaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
// val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
// val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx))
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx))
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty
|
||||
// assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc1-penalty
|
||||
// assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc2-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
Transaction.correctlySpends(penaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
// Transaction.correctlySpends(htlc1PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
// Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
|
||||
// two main outputs are 300 000 and 200 000
|
||||
// two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000
|
||||
assert(mainTx.txOut(0).amount == Satoshi(284950))
|
||||
assert(penaltyTx.txOut(0).amount == Satoshi(195170))
|
||||
assert(mainPenaltyTx.txOut(0).amount == Satoshi(195170))
|
||||
// assert(htlc1PenaltyTx.txOut(0).amount == Satoshi(194230))
|
||||
// assert(htlc2PenaltyTx.txOut(0).amount == Satoshi(294230))
|
||||
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
|
||||
|
|
|
@ -38,23 +38,24 @@ import scala.concurrent.duration._
|
|||
@RunWith(classOf[JUnitRunner])
|
||||
class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
|
||||
type FixtureParam = Tuple8[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, List[Transaction]]
|
||||
type FixtureParam = Tuple8[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, TestProbe, List[PublishableTxs]]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val setup = init()
|
||||
import setup._
|
||||
within(30 seconds) {
|
||||
|
||||
val bobCommitTxes = within(30 seconds) {
|
||||
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)
|
||||
val bobCommitTxes: List[Transaction] = (for (amt <- List(100000000, 200000000, 300000000)) yield {
|
||||
val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield {
|
||||
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
relayer.expectMsgType[ForwardAdd]
|
||||
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob)
|
||||
relayer.expectMsgType[ForwardFulfill]
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
relayer.expectMsgType[CommandBuffer.CommandAck]
|
||||
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
bobCommitTx1 :: bobCommitTx2 :: Nil
|
||||
}).flatten
|
||||
|
||||
|
@ -69,9 +70,9 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
// - revoked commit
|
||||
// and we want to be able to test the different scenarii.
|
||||
// Hence the NORMAL->CLOSING transition will occur in the individual tests.
|
||||
|
||||
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, bobCommitTxes))
|
||||
bobCommitTxes
|
||||
}
|
||||
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, bobCommitTxes))
|
||||
}
|
||||
|
||||
def mutualClose(alice: TestFSMRef[State, Data, Channel],
|
||||
|
@ -191,7 +192,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_HTLC_SPENT") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer, _) =>
|
||||
test("recv BITCOIN_OUTPUT_SPENT") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, relayer, _) =>
|
||||
within(30 seconds) {
|
||||
// alice sends an htlc to bob
|
||||
val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
|
||||
|
@ -229,20 +230,31 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (local commit)") { case (alice, _, _, _, alice2blockchain, _, _, _) =>
|
||||
test("recv BITCOIN_TX_CONFIRMED (local commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
// alice sends an htlc to bob
|
||||
val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
// an error occurs and alice publishes her commit tx
|
||||
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
alice ! Error("00" * 32, "oops".getBytes())
|
||||
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx))
|
||||
val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
|
||||
alice ! Error("00" * 32, "oops".getBytes)
|
||||
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) // commit tx
|
||||
val claimMainDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-delayed-output
|
||||
val htlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-timeout
|
||||
val claimDelayedTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-delayed-output
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(aliceCommitTx))
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // main-delayed-output
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].event.isInstanceOf[BITCOIN_TX_CONFIRMED]) // claim-delayed-output
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined)
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
|
||||
assert(initialState.localCommitPublished.isDefined)
|
||||
|
||||
// actual test starts here
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainDelayedTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcTimeoutTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimDelayedTx), 0, 0)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
@ -252,7 +264,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
|
||||
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
|
||||
val bobCommitTx = bobCommitTxes.last
|
||||
val bobCommitTx = bobCommitTxes.last.commitTx.tx
|
||||
assert(bobCommitTx.txOut.size == 2) // two main outputs
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
|
||||
|
@ -269,7 +281,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
|
||||
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
|
||||
val bobCommitTx = bobCommitTxes.last
|
||||
val bobCommitTx = bobCommitTxes.last.commitTx.tx
|
||||
assert(bobCommitTx.txOut.size == 2) // two main outputs
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
|
@ -333,15 +345,18 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
|
||||
// bob publishes one of his revoked txes
|
||||
val bobRevokedTx = bobCommitTxes.head
|
||||
val bobRevokedTx = bobCommitTxes.head.commitTx.tx
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
|
||||
|
||||
// alice publishes and watches the penalty tx
|
||||
alice2blockchain.expectMsgType[PublishAsap] // claim-main
|
||||
alice2blockchain.expectMsgType[PublishAsap] // main-penalty
|
||||
// alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
// alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState)
|
||||
|
@ -352,17 +367,71 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
within(30 seconds) {
|
||||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
// bob publishes multiple revoked txes (last one isn't revoked)
|
||||
for (bobRevokedTx <- bobCommitTxes.dropRight(1)) {
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
|
||||
// alice publishes and watches the penalty tx
|
||||
// alice publishes and watches the penalty tx
|
||||
alice2blockchain.expectMsgType[PublishAsap] // claim-main
|
||||
alice2blockchain.expectMsgType[PublishAsap] // main-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
}
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == bobCommitTxes.size - 1)
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(0).commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
alice2blockchain.expectMsgType[PublishAsap] // claim-main
|
||||
alice2blockchain.expectMsgType[PublishAsap] // main-penalty
|
||||
// alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
// alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(1).commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
alice2blockchain.expectMsgType[PublishAsap] // claim-main
|
||||
alice2blockchain.expectMsgType[PublishAsap] // main-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(2).commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
alice2blockchain.expectMsgType[PublishAsap] // claim-main
|
||||
alice2blockchain.expectMsgType[PublishAsap] // main-penalty
|
||||
// alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
// alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 3)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published HtlcSuccess tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, bobCommitTxes) =>
|
||||
within(30 seconds) {
|
||||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
// bob publishes one of his revoked txes
|
||||
val bobRevokedTx = bobCommitTxes.head
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main
|
||||
val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty
|
||||
// val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
// alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx)
|
||||
|
||||
// actual test starts here
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0)
|
||||
// alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) // we published this
|
||||
// alice2blockchain.expectMsgType[WatchConfirmed] // htlc-penalty
|
||||
// val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx
|
||||
// alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his HtlcSuccess tx
|
||||
// alice2blockchain.expectMsgType[WatchConfirmed] // htlc-success
|
||||
// val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx
|
||||
// alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx), 0, 0) // bob won
|
||||
// alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTxs), 0, 0) // bob won
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,19 +440,24 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
|
||||
// bob publishes one of his revoked txes
|
||||
val bobRevokedTx = bobCommitTxes.head
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main
|
||||
val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty
|
||||
// val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit
|
||||
alice2blockchain.expectMsgType[WatchConfirmed] // claim-main
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx)
|
||||
alice2blockchain.expectMsgType[WatchSpent] // main-penalty
|
||||
// alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx)
|
||||
|
||||
// actual test starts here
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0)
|
||||
// alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx)
|
||||
// alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcPenaltyTx), 0, 0)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,19 +16,29 @@
|
|||
|
||||
package fr.acinq.eclair.crypto
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.KeyPath
|
||||
import fr.acinq.bitcoin.{BinaryData, Block}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class LocalKeyManagerSpec extends FunSuite {
|
||||
test("generate the same node id") {
|
||||
test("generate the same node id from the same seed") {
|
||||
// if this test breaks it means that we will generate a different node id from
|
||||
// the same seed, which could be a problem during an upgrade
|
||||
val seed = BinaryData("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501")
|
||||
val keyManager = new LocalKeyManager(seed)
|
||||
val keyManager = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash)
|
||||
assert(keyManager.nodeId == PublicKey("02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee"))
|
||||
}
|
||||
test("generate different node ids from the same seed on different chains") {
|
||||
val seed = BinaryData("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501")
|
||||
val keyManager1 = new LocalKeyManager(seed, Block.TestnetGenesisBlock.hash)
|
||||
val keyManager2 = new LocalKeyManager(seed, Block.LivenetGenesisBlock.hash)
|
||||
assert(keyManager1.nodeId != keyManager2.nodeId)
|
||||
val keyPath = KeyPath(1L :: Nil)
|
||||
assert(keyManager1.fundingPublicKey(keyPath) != keyManager2.fundingPublicKey(keyPath))
|
||||
assert(keyManager1.commitmentPoint(keyPath, 1) != keyManager2.commitmentPoint(keyPath, 1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,15 @@
|
|||
package fr.acinq.eclair.db
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet, MilliSatoshi, Satoshi, Transaction}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, DeterministicWallet, MilliSatoshi, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.channel.Helpers.Funding
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment.{Local, Relayed}
|
||||
import fr.acinq.eclair.{UInt64, randomKey}
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, ChannelUpdate, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
|
@ -51,7 +49,7 @@ class ChannelStateSpec extends FunSuite {
|
|||
}
|
||||
|
||||
object ChannelStateSpec {
|
||||
val keyManager = new LocalKeyManager("01" * 32)
|
||||
val keyManager = new LocalKeyManager("01" * 32, Block.RegtestGenesisBlock.hash)
|
||||
val localParams = LocalParams(
|
||||
keyManager.nodeId,
|
||||
channelKeyPath = DeterministicWallet.KeyPath(Seq(42)),
|
||||
|
|
|
@ -18,10 +18,12 @@ package fr.acinq.eclair.db
|
|||
|
||||
import java.sql.DriverManager
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePendingRelayDb}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.sqlite.SQLiteException
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class SqliteChannelsDbSpec extends FunSuite {
|
||||
|
@ -41,12 +43,28 @@ class SqliteChannelsDbSpec extends FunSuite {
|
|||
|
||||
val channel = ChannelStateSpec.normal
|
||||
|
||||
val commitNumber = 42
|
||||
val paymentHash1 = BinaryData("42" * 300)
|
||||
val cltvExpiry1 = 123
|
||||
val paymentHash2 = BinaryData("43" * 300)
|
||||
val cltvExpiry2 = 656
|
||||
|
||||
intercept[SQLiteException](db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel
|
||||
|
||||
assert(db.listChannels().toSet === Set.empty)
|
||||
db.addOrUpdateChannel(channel)
|
||||
db.addOrUpdateChannel(channel)
|
||||
assert(db.listChannels() === List(channel))
|
||||
|
||||
assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == Nil)
|
||||
db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)
|
||||
db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash2, cltvExpiry2)
|
||||
assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == List((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2)))
|
||||
assert(db.listHtlcHtlcInfos(channel.channelId, 43).toList == Nil)
|
||||
|
||||
db.removeChannel(channel.channelId)
|
||||
assert(db.listChannels() === Nil)
|
||||
assert(db.listHtlcHtlcInfos(channel.channelId, commitNumber).toList == Nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import akka.pattern.pipe
|
|||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.google.common.net.HostAndPort
|
||||
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.bitcoin.Crypto.{PublicKey, sha256}
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
|
@ -35,8 +35,10 @@ import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
|||
import fr.acinq.eclair.io.Peer.Disconnect
|
||||
import fr.acinq.eclair.io.{NodeURI, Peer}
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _}
|
||||
import fr.acinq.eclair.payment.PaymentRequest
|
||||
import fr.acinq.eclair.payment.{LocalPaymentHandler, PaymentRequest}
|
||||
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, Kit, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
|
@ -61,7 +63,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
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 = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
|
@ -136,17 +138,21 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
|
||||
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))
|
||||
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.mindepth-blocks" -> 2, "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false))
|
||||
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.delay-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0)).withFallback(commonConfig)) // A's channels are private
|
||||
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.delay-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.delay-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.delay-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.delay-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
|
||||
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.delay-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
|
||||
instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.delay-blocks" -> 135, "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.delay-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.delay-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.delay-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.delay-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089)).withFallback(commonConfig))
|
||||
instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.delay-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
|
||||
// by default C has a normal payment handler, but this can be overriden in tests
|
||||
val paymentHandlerC = nodes("C").system.actorOf(LocalPaymentHandler.props(nodes("C").nodeParams))
|
||||
nodes("C").paymentHandler ! paymentHandlerC
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
|
@ -387,6 +393,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
|
||||
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// we subscribe to C's channel state transitions
|
||||
val stateListener = TestProbe()
|
||||
nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged])
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
|
@ -452,11 +461,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we generate blocks to make tx confirm
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and we wait for C'channel to close
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 9, 20)
|
||||
}
|
||||
|
||||
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// we subscribe to C's channel state transitions
|
||||
val stateListener = TestProbe()
|
||||
nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged])
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
|
@ -518,11 +535,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we generate blocks to make tx confirm
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and we wait for C'channel to close
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 8, 18)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// we subscribe to C's channel state transitions
|
||||
val stateListener = TestProbe()
|
||||
nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged])
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
|
@ -570,11 +595,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we generate blocks to make tx confirm
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and we wait for C'channel to close
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 7, 16)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// we subscribe to C's channel state transitions
|
||||
val stateListener = TestProbe()
|
||||
nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged])
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
|
@ -625,40 +658,97 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
(receivedByC diff previouslyReceivedByC).size == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we generate blocks to make tx confirm
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and we wait for C'channel to close
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds)
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 6, 14)
|
||||
}
|
||||
|
||||
test("punish a node that has published a revoked commit tx") {
|
||||
val sender = TestProbe()
|
||||
// we subscribe to C's channel state transitions
|
||||
val stateListener = TestProbe()
|
||||
nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged])
|
||||
// we use this to get commitments
|
||||
val sigListener = TestProbe()
|
||||
nodes("F5").system.eventStream.subscribe(sigListener.ref, classOf[ChannelSignatureReceived])
|
||||
// we use this to control when to fulfill htlcs, setup is as follow : noop-handler ---> forward-handler ---> payment-handler
|
||||
val forwardHandlerC = TestProbe()
|
||||
nodes("C").paymentHandler ! forwardHandlerC.ref
|
||||
val forwardHandlerF = TestProbe()
|
||||
nodes("F5").paymentHandler ! forwardHandlerF.ref
|
||||
// this is the actual payment handler that we will forward requests to
|
||||
val paymentHandlerC = nodes("C").system.actorOf(LocalPaymentHandler.props(nodes("C").nodeParams))
|
||||
val paymentHandlerF = nodes("F5").system.actorOf(LocalPaymentHandler.props(nodes("F5").nodeParams))
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// first we send 3 mBTC to F so that it has a balance
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("F5").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
sender.send(paymentHandlerF, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("F5").nodeParams.nodeId)
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, pr.nodeId)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// we forward the htlc to the payment handler
|
||||
forwardHandlerF.expectMsgType[UpdateAddHtlc]
|
||||
forwardHandlerF.forward(paymentHandlerF)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
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(Some(amountMsat1), "1 coffee"))
|
||||
val pr1 = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq1 = SendPayment(100000000L, pr1.paymentHash, nodes("F5").nodeParams.nodeId)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq1)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
// we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs
|
||||
def send(amountMsat: Long, paymentHandler: ActorRef, paymentInitiator: ActorRef) = {
|
||||
sender.send(paymentHandler, ReceivePayment(Some(MilliSatoshi(amountMsat)), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId)
|
||||
sender.send(paymentInitiator, sendReq)
|
||||
}
|
||||
val buffer = TestProbe()
|
||||
send(100000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending
|
||||
forwardHandlerF.expectMsgType[UpdateAddHtlc]
|
||||
forwardHandlerF.forward(buffer.ref)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
send(110000000, paymentHandlerF, nodes("C").paymentInitiator) // will be left pending
|
||||
forwardHandlerF.expectMsgType[UpdateAddHtlc]
|
||||
forwardHandlerF.forward(buffer.ref)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
send(120000000, paymentHandlerC, nodes("F5").paymentInitiator)
|
||||
forwardHandlerC.expectMsgType[UpdateAddHtlc]
|
||||
forwardHandlerC.forward(buffer.ref)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
send(130000000, paymentHandlerC, nodes("F5").paymentInitiator)
|
||||
forwardHandlerC.expectMsgType[UpdateAddHtlc]
|
||||
forwardHandlerC.forward(buffer.ref)
|
||||
val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments
|
||||
sigListener.expectNoMsg(1 second)
|
||||
// in this commitment, both parties should have a main output, and there are four pending htlcs
|
||||
val localCommitF = commitmentsF.localCommit.publishableTxs
|
||||
assert(localCommitF.commitTx.tx.txOut.size === 6)
|
||||
val htlcTimeoutTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: HtlcTimeoutTx, _, _) => h }
|
||||
val htlcSuccessTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: HtlcSuccessTx, _, _) => h }
|
||||
assert(htlcTimeoutTxs.size === 2)
|
||||
assert(htlcSuccessTxs.size === 2)
|
||||
// we fulfill htlcs to get the preimagse
|
||||
buffer.expectMsgType[UpdateAddHtlc]
|
||||
buffer.forward(paymentHandlerF)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
val preimage1 = sender.expectMsgType[PaymentSucceeded].paymentPreimage
|
||||
buffer.expectMsgType[UpdateAddHtlc]
|
||||
buffer.forward(paymentHandlerF)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
sender.expectMsgType[PaymentSucceeded].paymentPreimage
|
||||
buffer.expectMsgType[UpdateAddHtlc]
|
||||
buffer.forward(paymentHandlerC)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
sender.expectMsgType[PaymentSucceeded].paymentPreimage
|
||||
buffer.expectMsgType[UpdateAddHtlc]
|
||||
buffer.forward(paymentHandlerC)
|
||||
sigListener.expectMsgType[ChannelSignatureReceived]
|
||||
sender.expectMsgType[PaymentSucceeded].paymentPreimage
|
||||
// this also allows us to get the channel id
|
||||
val channelId = commitmentsF.channelId
|
||||
// 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)
|
||||
|
@ -666,16 +756,34 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
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", localCommitTxF.commitTx.tx.toString()))
|
||||
// F will publish the commitment above, which is now revoked
|
||||
val revokedCommitTx = localCommitF.commitTx.tx
|
||||
val htlcSuccess = Transactions.addSigs(htlcSuccessTxs.head.txinfo.asInstanceOf[HtlcSuccessTx], htlcSuccessTxs.head.localSig, htlcSuccessTxs.head.remoteSig, preimage1).tx
|
||||
val htlcTimeout = Transactions.addSigs(htlcTimeoutTxs.head.txinfo.asInstanceOf[HtlcTimeoutTx], htlcTimeoutTxs.head.localSig, htlcTimeoutTxs.head.remoteSig).tx
|
||||
Transaction.correctlySpends(htlcSuccess, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
Transaction.correctlySpends(htlcTimeout, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
// we then generate blocks to make the htlc timeout (nothing will happen in the channel because all of them have already been fulfilled)
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 20))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// then we publish F's revoked transactions
|
||||
sender.send(bitcoincli, BitcoinReq("sendrawtransaction", revokedCommitTx.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
|
||||
sender.send(bitcoincli, BitcoinReq("sendrawtransaction", htlcSuccess.toString()))
|
||||
sender.expectMsgType[JValue](10000 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("sendrawtransaction", htlcTimeout.toString()))
|
||||
sender.expectMsgType[JValue](10000 seconds)
|
||||
// at this point C should have 3 recv transactions: its previous main output, and F's main and htlc output (taken as 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
|
||||
(receivedByC diff previouslyReceivedByC).size == 6
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we generate blocks to make tx confirm
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// and we wait for C'channel to close
|
||||
awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds)
|
||||
// this will remove the channel
|
||||
awaitAnnouncements(nodes.filter(_._1 == "A"), 4, 5, 12)
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix
|
|||
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
|
||||
// alice and bob will both have 1 000 000 sat
|
||||
Globals.feeratesPerKw.set(FeeratesPerKw.single(10000))
|
||||
alice ! INPUT_INIT_FUNDER("00" * 32, 2000000, 1000000000, Globals.feeratesPerKw.get.block_1, Globals.feeratesPerKw.get.blocks_6, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty)
|
||||
alice ! INPUT_INIT_FUNDER("00" * 32, 2000000, 1000000000, Globals.feeratesPerKw.get.blocks_2, Globals.feeratesPerKw.get.blocks_6, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty)
|
||||
bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit)
|
||||
pipe ! (alice, bob)
|
||||
within(30 seconds) {
|
||||
|
|
|
@ -118,6 +118,20 @@ class TransactionsSpec extends FunSuite {
|
|||
assert(mainPenaltyWeight == weight)
|
||||
}
|
||||
|
||||
{
|
||||
// HtlcPenaltyTx
|
||||
// first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx
|
||||
val paymentPreimage = BinaryData("42" * 32)
|
||||
val htlc = UpdateAddHtlc("00" * 32, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), expiry = 400144, BinaryData.empty)
|
||||
val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.expiry)
|
||||
val pubKeyScript = write(pay2wsh(redeemScript))
|
||||
val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0)
|
||||
val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw)
|
||||
// we use dummy signatures to compute the weight
|
||||
val weight = Transaction.weight(addSigs(htlcPenaltyTx, "bb" * 71, localRevocationPriv.publicKey).tx)
|
||||
assert(htlcPenaltyWeight == weight)
|
||||
}
|
||||
|
||||
{
|
||||
// ClaimHtlcSuccessTx
|
||||
// first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx
|
||||
|
@ -276,30 +290,20 @@ class TransactionsSpec extends FunSuite {
|
|||
|
||||
{
|
||||
// remote spends offered HTLC output with revocation key
|
||||
val script = Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash))
|
||||
val index = commitTx.tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wsh(script)))
|
||||
val tx = Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(OutPoint(commitTx.tx, index), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(commitTx.tx.txOut(index).amount, Script.pay2wpkh(remotePaymentPriv.publicKey)) :: Nil,
|
||||
lockTime = 0)
|
||||
val sig = Transaction.signInput(tx, 0, script, SIGHASH_ALL, commitTx.tx.txOut(index).amount, SigVersion.SIGVERSION_WITNESS_V0, localRevocationPriv)
|
||||
val tx1 = tx.updateWitness(0, ScriptWitness(sig :: localRevocationPriv.publicKey.toBin :: Script.write(script) :: Nil))
|
||||
Transaction.correctlySpends(tx1, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
val script = Script.write(Scripts.htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc1.paymentHash)))
|
||||
val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, script, localDustLimit, finalPubKeyScript, feeratePerKw)
|
||||
val sig = sign(htlcPenaltyTx, localRevocationPriv)
|
||||
val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey)
|
||||
assert(checkSpendable(signed).isSuccess)
|
||||
}
|
||||
|
||||
{
|
||||
// remote spends received HTLC output with revocation key
|
||||
val script = Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.expiry)
|
||||
val index = commitTx.tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wsh(script)))
|
||||
val tx = Transaction(
|
||||
version = 2,
|
||||
txIn = TxIn(OutPoint(commitTx.tx, index), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(commitTx.tx.txOut(index).amount, Script.pay2wpkh(remotePaymentPriv.publicKey)) :: Nil,
|
||||
lockTime = 0)
|
||||
val sig = Transaction.signInput(tx, 0, script, SIGHASH_ALL, commitTx.tx.txOut(index).amount, SigVersion.SIGVERSION_WITNESS_V0, localRevocationPriv)
|
||||
val tx1 = tx.updateWitness(0, ScriptWitness(sig :: localRevocationPriv.publicKey.toBin :: Script.write(script) :: Nil))
|
||||
Transaction.correctlySpends(tx1, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
val script = Script.write(Scripts.htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, Crypto.ripemd160(htlc2.paymentHash), htlc2.expiry))
|
||||
val htlcPenaltyTx = makeHtlcPenaltyTx(commitTx.tx, script, localDustLimit, finalPubKeyScript, feeratePerKw)
|
||||
val sig = sign(htlcPenaltyTx, localRevocationPriv)
|
||||
val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey)
|
||||
assert(checkSpendable(signed).isSuccess)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -65,7 +65,7 @@
|
|||
<scala.version>2.11.11</scala.version>
|
||||
<scala.version.short>2.11</scala.version.short>
|
||||
<akka.version>2.3.14</akka.version>
|
||||
<bitcoinlib.version>0.9.14</bitcoinlib.version>
|
||||
<bitcoinlib.version>0.9.16</bitcoinlib.version>
|
||||
<guava.version>24.0-android</guava.version>
|
||||
</properties>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue