1
0
Fork 0
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:
pm47 2018-03-29 16:29:22 +02:00
commit e946c1a466
No known key found for this signature in database
GPG key ID: E434ED292E85643A
42 changed files with 1053 additions and 316 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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 _ => ???
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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] = (

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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