1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

Merge remote-tracking branch 'origin/wip-bolt2-nakamoto' into wip-bolt2

This commit is contained in:
sstone 2016-06-07 17:38:41 +02:00
commit 673cc61d6b
11 changed files with 600 additions and 456 deletions

View file

@ -1,7 +1,8 @@
# Testing eclair and lightningd
## Configure bitcoind to run in regtest mode
edit ~/.bitcoin/bitcoin.conf and add:
Important: you need a segwit version of bitcoin core for this test (see https://github.com/sipa/bitcoin/tree/segwit-master).
Make sure that bitcoin-cli is on the path and edit ~/.bitcoin/bitcoin.conf and add:
```shell
server=1
regtest=1
@ -9,17 +10,37 @@ rpcuser=***
rpcpassword=***
```
make sure that bitcoin-cli is on the path
To check that segwit is enabled run:
```shell
bitcoin-cli getblockchaininfo
```
and check bip9_softforks:
```
...
"bip9_softforks": {
"csv": {
"status": "active",
"startTime": 0,
"timeout": 999999999999
},
"witness": {
"status": "active",
"startTime": 0,
"timeout": 999999999999
}
}
```
## Start bitcoind
Mine a few blocks:
Mine enough blocks to activate segwit blocks:
```shell
bitcoin-cli generate 101
bitcoin-cli generate 500
```
##
Start lightningd (here well use port 50000)
Start lightningd (here well use port 46000)
```shell
lightningd --port 50000
lightningd --port 46000
```
##
Start eclair:
@ -31,7 +52,7 @@ mvn exec:java -Dexec.mainClass=fr.acinq.eclair.Boot
```shell
curl -X POST -H "Content-Type: application/json" -d '{
"method": "connect",
"params" : [ "localhost", 50000, 1000000 ]
"params" : [ "localhost", 46000, 3000000 ]
}' http://localhost:8080
```
Since eclair is funder, it will create and publish the anchor tx
@ -40,7 +61,19 @@ Mine a few blocks to confirm the anchor tx:
```shell
bitcoin-cli generate 10
```
eclair and lightningd are now both in NORMAL state (high priority for eclair, low priority for lightningd)
eclair and lightningd are now both in NORMAL state.
You can check this by running:
```shell
lightning-cli getpeers
```
or
```shell
curl -X POST -H "Content-Type: application/json" -d '{
"method": "list",
"params" : [ ]
}' http://localhost:8080
```
## Tell eclair to send a htlc
Well use the following values for R and H:
@ -53,19 +86,26 @@ Youll need a unix timestamp that is not too far into the future. Now + 100000
```shell
curl -X POST -H "Content-Type: application/json" -d "{
\"method\": \"addhtlc\",
\"params\" : [ \"1\", 100000, \"8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609\", $((`date +%s` + 100000)) ]
\"params\" : [ 70000000, \"8cf3e5f40cf025a984d8e00b307bbab2b520c91b2bde6fa86958f8f4e7d8a609\", $((`date +%s` + 100000)), \"021acf75c92318d3723098294d2a6a4b08d9abba2ebb5f2df2b4a8e9153e96a5f4\" ]
}" http://localhost:8080
```
## Tell eclair to commit its changes
```shell
curl -X POST -H "Content-Type: application/json" -d "{
\"method\": \"sign\",
\"params\" : [ \"d3f056a084e266ad06ea1ca28a1e080ca07c6b61fac7ce116e48a5c31d688eee\" ]
}" http://localhost:8080
```
## Tell lightningd to fulfill the HTLC:
```shell
./lightning-cli fulfillhtlc 0277863c1e40a2d4934ccf18e6679ea949d36bb0d1333fb098e99180df60d0195a 0102030405060708010203040506070801020304050607080102030405060708
./lightning-cli fulfillhtlc 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb 0102030405060708010203040506070801020304050607080102030405060708
```
Check balances on both eclair and lightningd
## Close the channel
```shell
./lightning-cli close 0277863c1e40a2d4934ccf18e6679ea949d36bb0d1333fb098e99180df60d0195a
./lightning-cli close 03befb4f8ad1d87d4c41acbb316791fe157f305caf2123c848f448975aaf85c1bb
```
Mine a few blocks to bury the closing tx
```shell

23
eclair-demo/eclair-cli Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
[ -z "$1" ] && (
echo "usage: "
echo "eclair-cli list"
echo "eclair-cli sign channel-id"
echo "eclair-cli fulfill channel-id htlc-id htlc-preimage"
) && exit 1
case $1 in
"list")
curl -X POST -d '{ "method": "list", "params" : [] }' "http://localhost:8080"
;;
"sign")
curl -X POST -d '{ "method": "sign", "params" : ["'${2?"missing channel id"}'"] }' "http://localhost:8080"
;;
"fulfill")
curl -X POST -d '{ "method": "fulfillhtlc", "params" : ["'${2?"missing channel id"}'", '${3?"missing htlc id"}', "'${4?"missing htlc preimage"}'"] }' "http://localhost:8080"
;;
esac
echo

View file

@ -75,10 +75,12 @@ trait Service extends Logging {
}
case JsonRPCBody(_, _, "sign", JString(channel) :: Nil) =>
sendCommand(channel, CMD_SIGN)
case JsonRPCBody(_, _, "fulfillhtlc", JString(channel) :: JDouble(id) :: JString(r) :: Nil) =>
case JsonRPCBody(_, _, "fulfillhtlc", JString(channel) :: JInt(id) :: JString(r) :: Nil) =>
sendCommand(channel, CMD_FULFILL_HTLC(id.toLong, BinaryData(r)))
case JsonRPCBody(_, _, "close", JString(channel) :: JString(scriptPubKey) :: Nil) =>
sendCommand(channel, CMD_CLOSE(Some(scriptPubKey)))
case JsonRPCBody(_, _, "close", JString(channel) :: Nil) =>
sendCommand(channel, CMD_CLOSE(None))
case JsonRPCBody(_, _, "help", _) =>
Future.successful(List(
"connect (host, port, anchor_amount): opens a channel with another eclair or lightningd instance",

View file

@ -54,7 +54,7 @@ class PollingWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContex
context.become(watching(watches - w))
case Publish(tx) =>
log.info(s"publishing tx $tx")
log.info(s"publishing tx ${tx.txid} $tx")
client.publishTransaction(tx).onFailure {
case t: Throwable => log.error(t, s"cannot publish tx ${Hex.toHexString(Transaction.write(tx, Protocol.PROTOCOL_VERSION | Transaction.SERIALIZE_TRANSACTION_WITNESS))}")
}

View file

@ -11,6 +11,8 @@ import fr.acinq.bitcoin.Crypto.sha256
import lightning._
import lightning.open_channel.anchor_offer.{WILL_CREATE_ANCHOR, WONT_CREATE_ANCHOR}
import scala.util.{Failure, Success, Try}
/**
* Created by PM on 20/08/2015.
*/
@ -24,8 +26,6 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
log.info(s"commit pubkey: ${params.commitPubKey}")
log.info(s"final pubkey: ${params.finalPubKey}")
val closeFee = 0L // TODO
params.anchorAmount match {
case None =>
them ! open_channel(params.delay, sha256(ShaChain.shaChainFromSeed(params.shaSeed, 0)), sha256(ShaChain.shaChainFromSeed(params.shaSeed, 1)), params.commitPubKey, params.finalPubKey, WONT_CREATE_ANCHOR, Some(params.minDepth), params.initialFeeRate)
@ -72,8 +72,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
when(OPEN_WAIT_FOR_ANCHOR) {
case Event(open_anchor(anchorTxHash, anchorOutputIndex, anchorAmount), DATA_OPEN_WAIT_FOR_ANCHOR(ourParams, theirParams, theirRevocationHash, theirNextRevocationHash)) =>
//see https://github.com/ElementsProject/lightning/issues/17
val anchorTxid = anchorTxHash.reverse
val anchorTxid = anchorTxHash.reverse //see https://github.com/ElementsProject/lightning/issues/17
val anchorOutput = TxOut(Satoshi(anchorAmount), publicKeyScript = Scripts.anchorPubkeyScript(ourParams.commitPubKey, theirParams.commitPubKey))
// they fund the channel with their anchor tx, so the money is theirs
@ -90,7 +90,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
blockchain ! WatchConfirmed(self, anchorTxid, ourParams.minDepth, BITCOIN_ANCHOR_DEPTHOK)
blockchain ! WatchSpent(self, anchorTxid, anchorOutputIndex, 0, BITCOIN_ANCHOR_SPENT)
// FIXME: ourTx is not signed by them and cannot be published
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, ShaChain.init, OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash), theirNextRevocationHash, None, anchorOutput)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(commitments, ShaChain.init, None)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
@ -101,33 +105,38 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
val theirSpec = theirCommitment.spec
// we build our commitment tx, sign it and check that it is spendable using the counterparty's sig
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, 0))
val ourSpec = CommitmentSpec(Set.empty[Htlc], feeRate = theirParams.initialFeeRate, initial_amount_us_msat = anchorAmount * 1000, initial_amount_them_msat = 0, amount_us_msat = anchorAmount * 1000, amount_them_msat = 0)
val ourSpec = CommitmentSpec(Set.empty[Htlc], feeRate = ourParams.initialFeeRate, initial_amount_us_msat = anchorAmount * 1000, initial_amount_them_msat = 0, amount_us_msat = anchorAmount * 1000, amount_them_msat = 0)
val ourTx = makeOurTx(ourParams, theirParams, TxIn(OutPoint(anchorTx, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, ourRevocationHash, ourSpec)
log.info(s"checking our tx: $ourTx")
val ourSig = sign(ourParams, theirParams, anchorAmount, ourTx)
val signedTx: Transaction = addSigs(ourParams, theirParams, anchorAmount, ourTx, ourSig, theirSig)
val anchorOutput = anchorTx.txOut(anchorOutputIndex)
checksig(ourParams, theirParams, anchorOutput, signedTx) match {
case false =>
case Failure(cause) =>
log.error(cause, "their open_commit_sig message contains an invalid signature")
them ! error(Some("Bad signature"))
goto(CLOSED)
case true =>
case Success(_) =>
blockchain ! WatchConfirmed(self, anchorTx.txid, ourParams.minDepth, BITCOIN_ANCHOR_DEPTHOK)
blockchain ! WatchSpent(self, anchorTx.txid, anchorOutputIndex, 0, BITCOIN_ANCHOR_SPENT)
blockchain ! Publish(anchorTx)
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(ourParams, theirParams, ShaChain.init, OurCommit(0, ourSpec, signedTx), theirCommitment, theirNextRevocationHash, None, anchorOutput)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, signedTx), theirCommitment,
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(commitments, ShaChain.init, None)
}
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
}
when(OPEN_WAITING_THEIRANCHOR) {
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(ourParams, theirParams, shaChain, ourCommit, theirCommit, theirNextRevocationHash, deferred, anchorOutput)) =>
blockchain ! WatchLost(self, d.asInstanceOf[CurrentCommitment].anchorId, ourParams.minDepth, BITCOIN_ANCHOR_LOST)
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(commitments, shaChain, deferred)) =>
blockchain ! WatchLost(self, commitments.anchorId, commitments.ourParams.minDepth, BITCOIN_ANCHOR_LOST)
them ! open_complete(None)
deferred.map(self ! _)
//TODO htlcIdx should not be 0 when resuming connection
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(ourParams, theirParams, shaChain, 0, ourCommit, theirCommit, OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), Some(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(commitments, shaChain, 0, None)
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
@ -147,25 +156,25 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event(BITCOIN_ANCHOR_SPENT, _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d@DATA_OPEN_WAITING(commitments, _, _)) =>
publish_ourcommit(commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(commitments, d.shaChain, ourCommitPublished = Some(commitments.ourCommit.publishableTx))
}
when(OPEN_WAITING_OURANCHOR) {
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(ourParams, theirParams, shaChain, ourCommit, theirCommit, theirNextRevocationHash, deferred, anchorOutput)) =>
blockchain ! WatchLost(self, d.asInstanceOf[CurrentCommitment].anchorId, ourParams.minDepth, BITCOIN_ANCHOR_LOST)
case Event(BITCOIN_ANCHOR_DEPTHOK, d@DATA_OPEN_WAITING(commitments, shaChain, deferred)) =>
blockchain ! WatchLost(self, commitments.anchorId, commitments.ourParams.minDepth, BITCOIN_ANCHOR_LOST)
them ! open_complete(None)
deferred.map(self ! _)
//TODO htlcIdx should not be 0 when resuming connection
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(ourParams, theirParams, shaChain, 0, ourCommit, theirCommit, OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), Some(theirNextRevocationHash), anchorOutput)
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(commitments, shaChain, 0, None)
case Event(msg@open_complete(blockId_opt), d: DATA_OPEN_WAITING) =>
log.info(s"received their open_complete, deferring message")
@ -181,21 +190,21 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_OPEN_WAITING) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
when(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) {
case Event(open_complete(blockid_opt), d: CurrentCommitment) =>
Register.create_alias(theirNodeId, d.anchorId)
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
/*case Event(pkt: close_channel, d: CurrentCommitment) =>
@ -208,22 +217,22 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_NORMAL) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
when(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) {
case Event(open_complete(blockid_opt), d: CurrentCommitment) =>
Register.create_alias(theirNodeId, d.anchorId)
case Event(open_complete(blockid_opt), d: DATA_NORMAL) =>
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
/*case Event(pkt: close_channel, d: CurrentCommitment) =>
@ -236,16 +245,16 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
them ! handle_cmd_close(cmd, d.ourParams, d.theirParams, d.commitment)
goto(WAIT_FOR_CLOSE_COMPLETE)*/
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isTheirCommit(tx, d.ourParams, d.theirParams, d.theirCommit)) =>
them ! handle_theircommit(tx, d.ourParams, d.theirParams, d.shaChain, d.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if (isTheirCommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.commitments.theirCommit)) =>
them ! handle_theircommit(tx, d.commitments.ourParams, d.commitments.theirParams, d.shaChain, d.commitments.theirCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, theirCommitPublished = Some(tx))
case Event((BITCOIN_ANCHOR_SPENT, _), _) =>
goto(ERR_INFORMATION_LEAK)
case Event(pkt: error, d: CurrentCommitment) =>
publish_ourcommit(d.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.ourCommit, d.theirCommit, ourCommitPublished = Some(d.ourCommit.publishableTx))
case Event(pkt: error, d: DATA_NORMAL) =>
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
@ -262,113 +271,83 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
when(NORMAL) {
case Event(CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin, id_opt), d@DATA_NORMAL(_, _, _, htlcIdx, _, _, ourChanges, _, _, _)) =>
case Event(CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin, id_opt), d@DATA_NORMAL(commitments, _, htlcIdx, _)) =>
// TODO: should we take pending htlcs into account?
// TODO: assert(commitment.state.commit_changes(staged).us.pay_msat >= amount, "insufficient funds!")
// TODO: nodeIds are ignored
val id: Long = id_opt.getOrElse(htlcIdx + 1)
val htlc = update_add_htlc(id, amount, rHash, expiry, routing(ByteString.EMPTY))
them ! htlc
stay using d.copy(htlcIdx = htlc.id, ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ htlc))
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(_, _, _, _, _, _, _, theirChanges, _, _)) =>
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _, _, _)) =>
// TODO: should we take pending htlcs into account?
// assert(commitment.state.commit_changes(staged).them.pay_msat >= amount, "insufficient funds!") // TODO : we should fail the channel
// TODO: nodeIds are ignored
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ htlc))
stay using d.copy(commitments = commitments.addTheirProposal(htlc))
case Event(CMD_FULFILL_HTLC(id, r), d@DATA_NORMAL(_, _, _, _, _, theirCommit, ourChanges, theirChanges, _, _)) =>
theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
val fulfill = update_fulfill_htlc(id, r)
them ! fulfill
stay using d.copy(ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
val (commitments1, fullfill) = Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))
them ! fullfill
stay using d.copy(commitments = commitments1)
case Event(fulfill@update_fulfill_htlc(id, r), d@DATA_NORMAL(_, _, _, _, ourCommit, _, ourChanges, theirChanges, _, _)) =>
ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(r)) =>
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ fulfill))
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc $id")
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_NORMAL) =>
stay using d.copy(commitments = Commitments.receiveFulfill(d.commitments, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d@DATA_NORMAL(_, _, _, _, _, theirCommit, ourChanges, theirChanges, _, _)) =>
theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
val fail = update_fail_htlc(id, fail_reason(ByteString.copyFromUtf8(reason)))
them ! fail
stay using d.copy(ourChanges = ourChanges.copy(proposed = ourChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id")
}
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
val (commitments1, fail) = Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))
them ! fail
stay using d.copy(commitments = commitments1)
case Event(fail@update_fail_htlc(id, reason), d@DATA_NORMAL(_, _, _, _, ourCommit, _, ourChanges, theirChanges, _, _)) =>
ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == id => u } match {
case Some(htlc) =>
stay using d.copy(theirChanges = theirChanges.copy(proposed = theirChanges.proposed :+ fail))
case None => throw new RuntimeException(s"unknown htlc id=$id") // TODO : we should fail the channel
}
case Event(fail@update_fail_htlc(id, reason), d: DATA_NORMAL) =>
stay using d.copy(commitments = Commitments.receiveFail(d.commitments, fail))
case Event(CMD_SIGN, d@DATA_NORMAL(ourParams, theirParams, _, _, ourCommit, theirCommit, ourChanges, theirChanges, theirNextRevocationHash_opt, anchorOutput)) =>
// sign all our proposals + their acked proposals
// their commitment now includes all our changes + their acked changes
theirNextRevocationHash_opt match {
case Some(theirNextRevocationHash) =>
val spec = reduce(theirCommit.spec, theirChanges.acked, ourChanges.acked ++ ourChanges.signed ++ ourChanges.proposed)
val theirTx = makeTheirTx(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, theirTx)
them ! update_commit(ourSig)
stay using d.copy(theirCommit = TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash), ourChanges = ourChanges.copy(proposed = Nil, signed = ourChanges.signed ++ ourChanges.proposed), theirNextRevocationHash = None)
case None => throw new RuntimeException(s"cannot send two update_commit in a row (must wait for revocation)")
}
case Event(CMD_SIGN, d: DATA_NORMAL) =>
val (commitments1, commit) = Commitments.sendCommit(d.commitments)
them ! commit
stay using d.copy(commitments = commitments1)
case Event(msg@update_commit(theirSig), d@DATA_NORMAL(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, anchorOutput)) =>
// we've received a signature
// ack all their changes
// our commitment now includes all theirs changes + our acked changes
val spec = reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 1))
val ourTx = makeOurTx(ourParams, theirParams, ourCommit.publishableTx.txIn, ourNextRevocationHash, spec)
val ourSig = sign(ourParams, theirParams, anchorOutput.amount.toLong, ourTx)
val signedTx = addSigs(ourParams, theirParams, anchorOutput.amount.toLong, ourTx, ourSig, theirSig)
checksig(ourParams, theirParams, anchorOutput, signedTx) match {
case false =>
case Event(msg@update_commit(theirSig), d: DATA_NORMAL) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) =>
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "received a bad signature")
them ! error(Some("Bad signature"))
publish_ourcommit(ourCommit)
goto(CLOSING) using DATA_CLOSING(ourParams, theirParams, shaChain, ourCommit, theirCommit, ourCommitPublished = Some(ourCommit.publishableTx))
case true =>
val ourRevocationPreimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index)
val ourRevocationHash = Crypto.sha256(ourRevocationPreimage)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 2))
them ! update_revocation(ourRevocationPreimage, ourNextRevocationHash)
val ourCommit1 = ourCommit.copy(index = ourCommit.index + 1, spec, publishableTx = signedTx)
stay using d.copy(ourCommit = ourCommit1, theirChanges = theirChanges.copy(proposed = Nil, acked = theirChanges.acked ++ theirChanges.proposed))
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_NORMAL(ourParams, theirParams, shaChain, _, ourCommit, theirCommit, ourChanges, theirChanges, _, _)) =>
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
//TODO : check rev pre image is valid
stay using d.copy(ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed), theirNextRevocationHash = Some(nextRevocationHash))
// TODO: check preimage
stay using d.copy(commitments = Commitments.receiveRevocation(d.commitments, msg))
/*case Event(CMD_CLOSE(scriptPubKeyOpt), d@DATA_NORMAL(ack_in, ack_out, ourParams, theirParams, shaChain, _, staged, commitment, nextCommitment)) =>
val scriptPubKey: BinaryData = scriptPubKeyOpt.getOrElse(Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash160(ourFinalPubKey.data)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil))
them ! close_clearing(scriptPubKey)
goto(CLOSE_CLEARING) using DATA_CLEARING(ack_in, ack_out + 1, ourParams, theirParams, shaChain, staged, commitment, nextCommitment, ClosingData(scriptPubKey, None))
case Event(clearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(ack_in, ack_out, ourParams, theirParams, shaChain, _, staged, commitment, nextCommitment)) =>
val ourScriptPubKey = ourParams.finalPubKey // TODO
them ! close_clearing(ourScriptPubKey)
if (commitment.state.them.htlcs_received.size == 0 && commitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, commitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using DATA_NEGOTIATING(ack_in + 1, ack_out + 1, ourParams, theirParams, shaChain, commitment, ClosingData(ourScriptPubKey, Some(theirScriptPubKey)))
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(commitments, _, _, ourClearingOpt)) =>
val ourClearing: close_clearing = ourClearingOpt.getOrElse {
val ourScriptPubKey: BinaryData = Script.write(Scripts.pay2pkh(commitments.ourParams.finalPubKey))
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.commitments.ourParams.finalPrivKey)}")
them ! close_clearing(ourScriptPubKey)
close_clearing(ourScriptPubKey)
}
if (commitments.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
goto(CLOSE_CLEARING) using DATA_CLEARING(ack_in + 1, ack_out + 1, ourParams, theirParams, shaChain, staged, commitment, nextCommitment, ClosingData(ourScriptPubKey, Some(theirScriptPubKey)))
}*/
goto(CLEARING) using DATA_CLEARING(commitments, d.shaChain, d.htlcIdx, ourClearing, theirClearing)
}
case Event(CMD_CLOSE(scriptPubKeyOpt), d: DATA_NORMAL) =>
val ourScriptPubKey: BinaryData = scriptPubKeyOpt.getOrElse {
log.info(s"our final tx can be redeemed with ${Base58Check.encode(Base58.Prefix.SecretKeyTestnet, d.commitments.ourParams.finalPrivKey)}")
Script.write(Scripts.pay2pkh(d.commitments.ourParams.finalPubKey))
}
val ourCloseClearing = close_clearing(ourScriptPubKey)
them ! ourCloseClearing
stay using d.copy(ourClearing = Some(ourCloseClearing))
/*case Event(pkt: close_channel, d: CurrentCommitment) =>
val (finalTx, res) = handle_pkt_close(pkt, d.ourParams, d.theirParams, d.commitment)
@ -393,46 +372,92 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
}
/*case Event(c@CMD_SEND_HTLC_FULFILL(r), DATA_NORMAL(ourParams, theirParams, shaChain, commitment@Commitment(_, _, previousState, _))) =>
// we paid upstream in exchange for r, now lets gets paid
Try(previousState.htlc_fulfill(r)) match {
case Success(newState) =>
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, commitment.index + 1))
// Complete your HTLC: I have the R value, pay me!
them ! update_fulfill_htlc(ourRevocationHash, r)
goto(WAIT_FOR_HTLC_ACCEPT(priority)) using DATA_WAIT_FOR_HTLC_ACCEPT(ourParams, theirParams, shaChain, commitment, UpdateProposal(commitment.index + 1, newState))
case Failure(t) =>
log.error(t, s"command $c failed")
stay
}*/
when(CLEARING) {
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
val (commitments1, fullfill) = Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))
them ! fullfill
stay using d.copy(commitments = commitments1)
/*case Event(update_fulfill_htlc(theirRevocationHash, r), DATA_NORMAL(ourParams, theirParams, shaChain, commitment)) =>
// FIXME: is this the right moment to propagate this htlc ?
// pm : probably not because if subsequent channel update fails we will already have paid the downstream channel
// and we'll get our money back only after the timeout
commitment.state.them.htlcs_received.find(_.rHash == bin2sha256(Crypto.sha256(r)))
.map(htlc => htlc.previousChannelId match {
case Some(previousChannelId) =>
log.info(s"resolving channelId=$previousChannelId")
Boot.system.actorSelection(Register.actorPathToChannelId(previousChannelId))
.resolveOne(3 seconds)
.onComplete {
case Success(downstream) =>
log.info(s"forwarding r value to downstream=$downstream")
downstream ! CMD_SEND_HTLC_FULFILL(r)
case Failure(t: Throwable) =>
log.warning(s"couldn't resolve downstream node, htlc will timeout", t)
}
case None =>
log.info(s"looks like I was the origin payer for htlc $htlc")
})
val newState = commitment.state.htlc_fulfill(r)
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, commitment.index + 1))
val (newCommitmentTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, commitment.tx.txIn, newState, ourRevocationHash, theirRevocationHash)
them ! update_accept(ourSigForThem, ourRevocationHash)
goto(WAIT_FOR_UPDATE_SIG(priority)) using DATA_WAIT_FOR_UPDATE_SIG(ourParams, theirParams, shaChain, commitment, Commitment(commitment.index + 1, newCommitmentTx, newState, theirRevocationHash))*/
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_CLEARING) =>
stay using d.copy(commitments = Commitments.receiveFulfill(d.commitments, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
val (commitments1, fail) = Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))
them ! fail
stay using d.copy(commitments = commitments1)
case Event(fail@update_fail_htlc(id, reason), d: DATA_CLEARING) =>
stay using d.copy(commitments = Commitments.receiveFail(d.commitments, fail))
case Event(CMD_SIGN, d: DATA_CLEARING) =>
val (commitments1, commit) = Commitments.sendCommit(d.commitments)
them ! commit
stay using d.copy(commitments = commitments1)
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, _, _, ourClearing, theirClearing)) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) =>
them ! revocation
if (commitments1.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments1, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
stay using d.copy(commitments = commitments1)
}
case Failure(cause) =>
log.error(cause, "received a bad signature")
them ! error(Some("Bad signature"))
publish_ourcommit(d.commitments.ourCommit)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, _, _, ourClearing, theirClearing)) =>
val commitments1 = Commitments.receiveRevocation(commitments, msg)
if (commitments1.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOCIATING) using DATA_NEGOCIATING(commitments1, d.shaChain, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
stay using d.copy(commitments = commitments1)
}
}
when(NEGOCIATING) {
case Event(close_signature(theirCloseFee, theirSig), d: DATA_NEGOCIATING) if theirCloseFee == d.ourSignature.closeFee =>
checkCloseSignature(theirSig, Satoshi(theirCloseFee), d) match {
case Success(signedTx) =>
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
case Failure(cause) =>
log.error(cause, "cannot verify their close signature")
throw new RuntimeException("cannot verify their close signature", cause)
}
case Event(close_signature(theirCloseFee, theirSig), d: DATA_NEGOCIATING) =>
checkCloseSignature(theirSig, Satoshi(theirCloseFee), d) match {
case Success(_) =>
val closeFee = ((theirCloseFee + d.ourSignature.closeFee) / 4) * 2 match {
case value if value == d.ourSignature.closeFee => value + 2
case value => value
}
val (finalTx, ourCloseSig) = makeFinalTx(d.commitments, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, Satoshi(closeFee))
them ! ourCloseSig
if (closeFee == theirCloseFee) {
val signedTx = addSigs(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput.amount.toLong, finalTx, ourCloseSig.sig, theirSig)
blockchain ! Publish(signedTx)
blockchain ! WatchConfirmed(self, signedTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, d.shaChain, mutualClosePublished = Some(signedTx))
} else {
stay using d.copy(ourSignature = ourCloseSig)
}
case Failure(cause) =>
log.error(cause, "cannot verify their close signature")
throw new RuntimeException("cannot verify their close signature", cause)
}
}
/*
.d8888b. 888 .d88888b. .d8888b. 8888888 888b 888 .d8888b.
d88P Y88b 888 d88P" "Y88b d88P Y88b 888 8888b 888 d88P Y88b
@ -444,178 +469,18 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
"Y8888P" 88888888 "Y88888P" "Y8888P" 8888888 888 Y888 "Y8888P88
*/
/*def clearing_handler: StateFunction = {
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
// TODO : should we take pending htlcs into account?
assert(commitment.state.commit_changes(staged).them.pay_msat >= amount, "insufficient funds!") // TODO : we should fail the channel
// TODO nodeIds are ignored
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, htlc))
when(CLOSING) {
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) if d.ourSignature.map(_.closeFee) == Some(theirCloseFee) =>
stay()
case Event(fulfill@update_fulfill_htlc(id, r), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fulfill))
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) =>
throw new RuntimeException(s"unexpected closing fee: $theirCloseFee ours is ${d.ourSignature.map(_.closeFee)}")
case Event(fail@update_fail_htlc(id, reason), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fail))
case Event(CMD_FULFILL_HTLC(id, r), d@DATA_CLEARING(_, ack_out, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).us.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
val fulfill = update_fulfill_htlc(id, r)
them ! fulfill
stay using d.copy(ack_out = ack_out + 1, staged = staged :+ Change(OUT, ack_out + 1, fulfill))
case Event(fulfill@update_fulfill_htlc(id, r), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fulfill))
case Event(CMD_FAIL_HTLC(id, reason), d@DATA_CLEARING(_, ack_out, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).us.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
val fail = update_fail_htlc(id, fail_reason(ByteString.copyFromUtf8(reason)))
them ! fail
stay using d.copy(ack_out = ack_out + 1, staged = staged :+ Change(OUT, ack_out + 1, fail))
case Event(fail@update_fail_htlc(id, reason), d@DATA_CLEARING(ack_in, _, _, _, _, staged, commitment, _, _)) =>
assert(commitment.state.commit_changes(staged).them.htlcs_received.exists(_.id == id), s"unknown htlc id=$id") // TODO : we should fail the channel
stay using d.copy(ack_in = ack_in + 1, staged = staged :+ Change(IN, ack_in + 1, fail))
case Event(clearing@close_clearing(theirScriptPubKey), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, commitment, _, ClosingData(_, None))) =>
val closing = d.closing.copy(theirScriptPubKey = Some(theirScriptPubKey))
if (commitment.state.them.htlcs_received.size == 0 && commitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, commitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using DATA_NEGOTIATING(ack_in + 1, ack_out + 1, ourParams, theirParams, shaChain, commitment, closing)
} else {
stay using d.copy(ack_in = ack_in + 1, closing = closing)
}
}
when(CLOSE_CLEARING)(clearing_handler orElse {
case Event(CMD_SIGN, d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, ReadyForSig(theirNextRevocationHash), _)) =>
val proposal = UpdateProposal(previousCommitment.index + 1, previousCommitment.state.commit_changes(staged), theirNextRevocationHash)
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, theirNextRevocationHash)
them ! update_commit(ourSigForThem, ack_in)
goto(CLOSE_CLEARING_WAIT_FOR_REV) using d.copy(ack_out = ack_out + 1, staged = Nil, next = WaitForRev(proposal))
case Event(msg@update_commit(theirSig, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, ReadyForSig(theirNextRevocationHash), _)) =>
// counterparty initiated a new commitment
val committed_changes = staged.filter(c => c.direction == IN || c.ack <= theirAck)
val uncommitted_changes = staged.filterNot(committed_changes.contains(_))
// TODO : we should check that this is the correct state (see acknowledge discussion)
val proposal = UpdateProposal(previousCommitment.index + 1, previousCommitment.state.commit_changes(committed_changes), theirNextRevocationHash)
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, proposal.theirRevocationHash)
val signedCommitTx = sign_our_commitment_tx(ourParams, theirParams, ourCommitTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedCommitTx, Map(previousCommitment.tx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
case true =>
val preimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, previousCommitment.index)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index + 1))
them ! update_revocation(preimage, ourNextRevocationHash, ack_in + 1)
them ! update_commit(ourSigForThem, ack_in + 1)
goto(CLOSE_CLEARING_WAIT_FOR_REV_THEIRSIG) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 2, staged = uncommitted_changes, next = WaitForRevTheirSig(Commitment(proposal.index, signedCommitTx, proposal.state, proposal.theirRevocationHash)))
}
})
when(CLOSE_CLEARING_WAIT_FOR_REV)(clearing_handler orElse {
case Event(update_revocation(theirRevocationPreimage, theirNextRevocationHash, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRev(proposal), closing)) =>
// counterparty replied with the signature for its new commitment tx, and revocationPreimage
val revocationHashCheck = new BinaryData(previousCommitment.theirRevocationHash) == new BinaryData(Crypto.sha256(theirRevocationPreimage))
if (revocationHashCheck) {
goto(CLOSE_CLEARING_WAIT_FOR_SIG) using d.copy(ack_in = ack_in + 1, next = WaitForSig(proposal, theirNextRevocationHash))
} else {
log.warning(s"the revocation preimage they gave us is wrong! hash=${previousCommitment.theirRevocationHash} preimage=$theirRevocationPreimage")
them ! error(Some("Wrong preimage"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
}
case Event(msg@update_commit(theirSig, theirAck), DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRev(proposal), closing)) =>
// TODO : IGNORED FOR NOW
log.warning(s"ignored $msg")
stay
})
when(CLOSE_CLEARING_WAIT_FOR_REV_THEIRSIG)(clearing_handler orElse {
case Event(update_revocation(theirRevocationPreimage, theirNextRevocationHash, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForRevTheirSig(nextCommitment), _)) =>
// counterparty replied with the signature for its new commitment tx, and revocationPreimage
val revocationHashCheck = new BinaryData(previousCommitment.theirRevocationHash) == new BinaryData(Crypto.sha256(theirRevocationPreimage))
if (revocationHashCheck) {
if (nextCommitment.state.them.htlcs_received.size == 0 && nextCommitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(nextCommitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, nextCommitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
} else {
goto(CLOSE_CLEARING) using d.copy(ack_in = ack_in + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
}
} else {
log.warning(s"the revocation preimage they gave us is wrong! hash=${previousCommitment.theirRevocationHash} preimage=$theirRevocationPreimage")
them ! error(Some("Wrong preimage"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
}
})
when(CLOSE_CLEARING_WAIT_FOR_SIG)(clearing_handler orElse {
case Event(update_commit(theirSig, theirAck), d@DATA_CLEARING(ack_in, ack_out, ourParams, theirParams, shaChain, staged, previousCommitment, WaitForSig(proposal, theirNextRevocationHash), _)) =>
// counterparty replied with the signature for the new commitment tx
val ourRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index))
val (ourCommitTx, ourSigForThem) = sign_their_commitment_tx(ourParams, theirParams, previousCommitment.tx.txIn, proposal.state, ourRevocationHash, proposal.theirRevocationHash)
val signedCommitTx = sign_our_commitment_tx(ourParams, theirParams, ourCommitTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedCommitTx, Map(previousCommitment.tx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(previousCommitment)
goto(CLOSING) using DATA_CLOSING(ack_in = ack_in + 1, ack_out = ack_out + 1, ourParams, theirParams, shaChain, previousCommitment, ourCommitPublished = Some(previousCommitment.tx))
case true =>
val preimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, previousCommitment.index)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, proposal.index + 1))
them ! update_revocation(preimage, ourNextRevocationHash, ack_in + 1)
val nextCommitment = Commitment(proposal.index, signedCommitTx, proposal.state, proposal.theirRevocationHash)
if (nextCommitment.state.them.htlcs_received.size == 0 && nextCommitment.state.us.htlcs_received.size == 0) {
val finalTx = makeFinalTx(nextCommitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, nextCommitment.state) //TODO ADJUST FEES
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
them ! close_signature(closeFee, ourSig)
goto(CLOSE_NEGOTIATING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
} else {
goto(CLOSE_CLEARING) using d.copy(ack_in = ack_in + 1, ack_out = ack_out + 1, commitment = nextCommitment, next = ReadyForSig(theirNextRevocationHash))
}
}
})
when(CLOSE_NEGOTIATING) {
case Event(close_signature(closeFee, sig), DATA_NEGOTIATING(ack_in, ack_out, ourParams, theirParams, shaChain, commitment, closing)) =>
// TODO: we should actually negotiate
// TODO: publish tx
val mutualTx: Transaction = null // TODO
goto(CLOSING) using DATA_CLOSING(ack_in, ack_out, ourParams, theirParams, shaChain, commitment, Some(mutualTx), None, None, Nil)
case Event(BITCOIN_CLOSE_DONE, _) => goto(CLOSED)
}
/*
/*when(WAIT_FOR_CLOSE_COMPLETE) {
case Event(close_channel_complete(theirSig), d: CurrentCommitment) =>
//TODO we should use the closing fee in pkts
val closingState = d.commitment.state.adjust_fees(Globals.closing_fee * 1000, d.ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(d.commitment.tx.txIn, ourFinalPubKey, d.theirParams.finalPubKey, closingState)
val signedFinalTx = sign_our_commitment_tx(d.ourParams, d.theirParams, finalTx, theirSig)
val ok = Try(Transaction.correctlySpends(signedFinalTx, Map(signedFinalTx.txIn(0).outPoint -> anchorPubkeyScript(ourCommitPubKey, d.theirParams.commitPubKey)), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
ok match {
case false =>
them ! error(Some("Bad signature"))
publish_ourcommit(d.commitment)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.commitment, ourCommitPublished = Some(d.commitment.tx))
case true =>
them ! close_channel_ack()
blockchain ! Publish(signedFinalTx)
goto(CLOSING) using DATA_CLOSING(d.ourParams, d.theirParams, d.shaChain, d.commitment, mutualClosePublished = Some(signedFinalTx))
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: CurrentCommitment) if (isMutualClose(tx, d.ourParams, d.theirParams, d.commitment)) =>
// it is possible that we received this before the close_channel_complete, we may still receive the latter
@ -739,7 +604,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case Event(CMD_GETINFO, _) =>
sender ! RES_GETINFO(theirNodeId, stateData match {
case c: CurrentCommitment => c.anchorId
case c: DATA_NORMAL => c.commitments.anchorId
case c: DATA_OPEN_WAITING => c.commitments.anchorId
case c: DATA_CLEARING => c.commitments.anchorId
case c: DATA_NEGOCIATING => c.commitments.anchorId
case c: DATA_CLOSING => c.commitments.anchorId
case _ => Hash.Zeroes
}, stateName, stateData)
stay
@ -802,6 +671,12 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
// wait for BITCOIN_STEAL_DONE
error(Some("Otherspend noticed"))
}
def checkCloseSignature(closeSig: BinaryData, closeFee: Satoshi, d: DATA_NEGOCIATING): Try[Transaction] = {
val (finalTx, ourCloseSig) = Helpers.makeFinalTx(d.commitments, d.ourClearing.scriptPubkey, d.theirClearing.scriptPubkey, closeFee)
val signedTx = addSigs(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput.amount.toLong, finalTx, ourCloseSig.sig, closeSig)
checksig(d.commitments.ourParams, d.commitments.theirParams, d.commitments.anchorOutput, signedTx).map(_ => signedTx)
}
}

View file

@ -3,7 +3,7 @@ package fr.acinq.eclair.channel
import com.trueaccord.scalapb.GeneratedMessage
import fr.acinq.bitcoin.{BinaryData, Crypto, Transaction, TxOut}
import fr.acinq.eclair.crypto.ShaChain
import lightning.{locktime, open_complete, sha256_hash}
import lightning._
/**
* Created by PM on 20/05/2016.
@ -33,12 +33,8 @@ case object OPEN_WAITING_OURANCHOR extends State
case object OPEN_WAIT_FOR_COMPLETE_OURANCHOR extends State
case object OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR extends State
case object NORMAL extends State
case object CLOSE_CLEARING extends State
case object CLOSE_CLEARING_WAIT_FOR_REV extends State
case object CLOSE_CLEARING_WAIT_FOR_REV_THEIRSIG extends State
case object CLOSE_CLEARING_WAIT_FOR_SIG extends State
case object CLOSE_NEGOTIATING extends State
case object WAIT_FOR_CLOSE_COMPLETE extends State
case object CLEARING extends State
case object NEGOCIATING extends State
case object CLOSING extends State
case object CLOSED extends State
case object ERR_ANCHOR_LOST extends State
@ -126,33 +122,6 @@ final case class CommitmentSpec(htlcs: Set[Htlc], feeRate: Long, initial_amount_
val totalFunds = amount_us_msat + amount_them_msat + htlcs.toSeq.map(_.amountMsat).sum
}
trait CurrentCommitment {
def ourParams: OurChannelParams
def theirParams: TheirChannelParams
def shaChain: ShaChain
def ourCommit: OurCommit
def theirCommit: TheirCommit
def anchorId: BinaryData = {
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
ourCommit.publishableTx.txIn(0).outPoint.hash
}
}
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
final case class DATA_OPEN_WAIT_FOR_OPEN (ourParams: OurChannelParams) extends Data
final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: BinaryData, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_ANCHOR (ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: sha256_hash, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_COMMIT_SIG (ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorTx: Transaction, anchorOutputIndex: Int, initialCommitment: TheirCommit, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAITING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, ourCommit: OurCommit, theirCommit: TheirCommit, theirNextRevocationHash: sha256_hash, deferred: Option[open_complete], anchorOutput: TxOut) extends Data with CurrentCommitment
final case class DATA_NORMAL (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, htlcIdx: Long,
ourCommit: OurCommit,
theirCommit: TheirCommit,
ourChanges: OurChanges,
theirChanges: TheirChanges,
theirNextRevocationHash: Option[sha256_hash],
anchorOutput: TxOut) extends Data with CurrentCommitment
object TypeDefs {
type Change = GeneratedMessage
}
@ -163,10 +132,25 @@ case class Changes(ourChanges: OurChanges, theirChanges: TheirChanges)
case class OurCommit(index: Long, spec: CommitmentSpec, publishableTx: Transaction)
case class TheirCommit(index: Long, spec: CommitmentSpec, theirRevocationHash: sha256_hash)
/*final case class DATA_CLEARING (ack_in: Long, ack_out: Long, ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, staged: List[Change], commitment: Commitment, next: NextCommitment, closing: ClosingData) extends Data with CurrentCommitment
final case class DATA_NEGOTIATING (ack_in: Long, ack_out: Long, ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, commitment: Commitment, closing: ClosingData) extends Data with CurrentCommitment
*/final case class DATA_CLOSING (ourParams: OurChannelParams, theirParams: TheirChannelParams, shaChain: ShaChain, ourCommit: OurCommit, theirCommit: TheirCommit,
mutualClosePublished: Option[Transaction] = None, ourCommitPublished: Option[Transaction] = None, theirCommitPublished: Option[Transaction] = None, revokedPublished: Seq[Transaction] = Seq()) extends Data with CurrentCommitment {
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
final case class DATA_OPEN_WAIT_FOR_OPEN (ourParams: OurChannelParams) extends Data
final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: BinaryData, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_ANCHOR (ourParams: OurChannelParams, theirParams: TheirChannelParams, theirRevocationHash: sha256_hash, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAIT_FOR_COMMIT_SIG (ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorTx: Transaction, anchorOutputIndex: Int, initialCommitment: TheirCommit, theirNextRevocationHash: sha256_hash) extends Data
final case class DATA_OPEN_WAITING (commitments: Commitments, shaChain: ShaChain, deferred: Option[open_complete]) extends Data
final case class DATA_NORMAL (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: Option[close_clearing]) extends Data
final case class DATA_CLEARING (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing) extends Data
final case class DATA_NEGOCIATING (commitments: Commitments, shaChain: ShaChain, htlcIdx: Long,
ourClearing: close_clearing, theirClearing: close_clearing, ourSignature: close_signature) extends Data
final case class DATA_CLOSING (commitments: Commitments, shaChain: ShaChain,
ourSignature: Option[close_signature] = None,
mutualClosePublished: Option[Transaction] = None,
ourCommitPublished: Option[Transaction] = None,
theirCommitPublished: Option[Transaction] = None,
revokedPublished: Seq[Transaction] = Seq()) extends Data {
assert(mutualClosePublished.isDefined || ourCommitPublished.isDefined || theirCommitPublished.isDefined || revokedPublished.size > 0, "there should be at least one tx published in this state")
}

View file

@ -0,0 +1,153 @@
package fr.acinq.eclair.channel
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction, TxOut}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.TypeDefs.Change
import fr.acinq.eclair.crypto.ShaChain
import lightning._
/**
* about theirNextCommitInfo:
* we either:
* - have built and sign their next commit tx with their next revocation hash which can now be discarded
* - have their next revocation hash
* So, when we've signed and sent a commit message and are waiting for their revocation message,
* theirNextCommitInfo is their next commit tx. The rest of the time, it is their next revocation hash
*/
case class Commitments(ourParams: OurChannelParams, theirParams: TheirChannelParams,
ourCommit: OurCommit, theirCommit: TheirCommit,
ourChanges: OurChanges, theirChanges: TheirChanges,
theirNextCommitInfo: Either[TheirCommit, BinaryData],
anchorOutput: TxOut) {
def anchorId: BinaryData = {
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
ourCommit.publishableTx.txIn(0).outPoint.hash
}
def hasNoPendingHtlcs: Boolean = ourCommit.spec.htlcs.isEmpty && theirCommit.spec.htlcs.isEmpty
def addOurProposal(proposal: Change): Commitments = Commitments.addOurProposal(this, proposal)
def addTheirProposal(proposal: Change): Commitments = Commitments.addTheirProposal(this, proposal)
}
object Commitments {
/**
* add a change to our proposed change list
*
* @param commitments
* @param proposal
* @return an updated commitment instance
*/
def addOurProposal(commitments: Commitments, proposal: Change): Commitments =
commitments.copy(ourChanges = commitments.ourChanges.copy(proposed = commitments.ourChanges.proposed :+ proposal))
def addTheirProposal(commitments: Commitments, proposal: Change): Commitments =
commitments.copy(theirChanges = commitments.theirChanges.copy(proposed = commitments.theirChanges.proposed :+ proposal))
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, update_fulfill_htlc) = {
commitments.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == cmd.id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(cmd.r)) =>
val fulfill = update_fulfill_htlc(cmd.id, cmd.r)
val commitments1 = addOurProposal(commitments, fulfill)
(commitments1, fulfill)
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc ${cmd.id}")
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
}
}
def receiveFulfill(commitments: Commitments, fulfill: update_fulfill_htlc): Commitments = {
commitments.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == fulfill.id => u } match {
case Some(htlc) if htlc.rHash == bin2sha256(Crypto.sha256(fulfill.r)) => addTheirProposal(commitments, fulfill)
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc ${fulfill.id}")
case None => throw new RuntimeException(s"unknown htlc id=${fulfill.id}") // TODO : we should fail the channel
}
}
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC): (Commitments, update_fail_htlc) = {
commitments.theirChanges.acked.collectFirst { case u: update_add_htlc if u.id == cmd.id => u } match {
case Some(htlc) =>
val fail = update_fail_htlc(cmd.id, fail_reason(ByteString.copyFromUtf8(cmd.reason)))
val commitments1 = addOurProposal(commitments, fail)
(commitments1, fail)
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
}
}
def receiveFail(commitments: Commitments, fail: update_fail_htlc): Commitments = {
commitments.ourChanges.acked.collectFirst { case u: update_add_htlc if u.id == fail.id => u } match {
case Some(htlc) =>
addTheirProposal(commitments, fail)
case None => throw new RuntimeException(s"unknown htlc id=${fail.id}") // TODO : we should fail the channel
}
}
def sendCommit(commitments: Commitments): (Commitments, update_commit) = {
import commitments._
commitments.theirNextCommitInfo match {
case Right(theirNextRevocationHash) =>
// sign all our proposals + their acked proposals
// their commitment now includes all our changes + their acked changes
val spec = Helpers.reduce(theirCommit.spec, theirChanges.acked, ourChanges.acked ++ ourChanges.signed ++ ourChanges.proposed)
val theirTx = Helpers.makeTheirTx(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
val ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount.toLong, theirTx)
val commit = update_commit(ourSig)
val commitments1 = commitments.copy(
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash)),
ourChanges = ourChanges.copy(proposed = Nil, signed = ourChanges.signed ++ ourChanges.proposed))
(commitments1, commit)
case Left(theirNextCommit) =>
throw new RuntimeException("attempting to sign twice waiting for the first revocation message")
}
}
def receiveCommit(commitments: Commitments, commit: update_commit): (Commitments, update_revocation) = {
import commitments._
// they sent us a signature for *their* view of *our* next commit tx
// so in terms of rev.hashes and indexes we have:
// ourCommit.index -> our current revocation hash, which is about to become our old revocation hash
// ourCommit.index + 1 -> our next revocation hash, used by * them * to build the sig we've just received, and which
// is about to become our current revocation hash
// ourCommit.index + 2 -> which is about to become our next revocation hash
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
// and will increment our index
// check that their signature is valid
val spec = Helpers.reduce(ourCommit.spec, ourChanges.acked, theirChanges.acked ++ theirChanges.proposed)
val ourNextRevocationHash = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 1))
val ourTx = Helpers.makeOurTx(ourParams, theirParams, ourCommit.publishableTx.txIn, ourNextRevocationHash, spec)
val ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount.toLong, ourTx)
val signedTx = Helpers.addSigs(ourParams, theirParams, anchorOutput.amount.toLong, ourTx, ourSig, commit.sig)
Helpers.checksig(ourParams, theirParams, anchorOutput, signedTx).get
// we will send our revocation preimage+ our next revocation hash
val ourRevocationPreimage = ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index)
val ourNextRevocationHash1 = Crypto.sha256(ShaChain.shaChainFromSeed(ourParams.shaSeed, ourCommit.index + 2))
val revocation = update_revocation(ourRevocationPreimage, ourNextRevocationHash1)
// update our commitment data
val ourCommit1 = ourCommit.copy(index = ourCommit.index + 1, spec, publishableTx = signedTx)
val theirChanges1 = theirChanges.copy(proposed = Nil, acked = theirChanges.acked ++ theirChanges.proposed)
val commitments1 = commitments.copy(ourCommit = ourCommit1, theirChanges = theirChanges1)
(commitments1, revocation)
}
def receiveRevocation(commitments: Commitments, revocation: update_revocation): Commitments = {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
theirNextCommitInfo match {
case Left(theirNextCommit) =>
assert(BinaryData(Crypto.sha256(revocation.revocationPreimage)) == BinaryData(theirCommit.theirRevocationHash), "invalid preimage")
commitments.copy(
ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed),
theirCommit = theirNextCommit,
theirNextCommitInfo = Right(revocation.nextRevocationHash))
case Right(_) =>
throw new RuntimeException("received unexpected update_revocation message")
}
}
}

View file

@ -85,8 +85,8 @@ object Helpers {
tx.copy(witness = Seq(witness))
}
def checksig(ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorOutput: TxOut, tx: Transaction): Boolean =
Try(Transaction.correctlySpends(tx, Map(tx.txIn(0).outPoint -> anchorOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isSuccess
def checksig(ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorOutput: TxOut, tx: Transaction): Try[Unit] =
Try(Transaction.correctlySpends(tx, Map(tx.txIn(0).outPoint -> anchorOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
def isMutualClose(tx: Transaction, ourParams: OurChannelParams, theirParams: TheirChannelParams, commitment: OurCommit): Boolean = {
// we rebuild the closing tx as seen by both parties
@ -113,20 +113,33 @@ object Helpers {
true
}
/*def handle_cmd_close(cmd: CMD_CLOSE, ourParams: OurChannelParams, theirParams: TheirChannelParams, commitment: Commitment): close_channel = {
val closingState = commitment.state.adjust_fees(cmd.fee * 1000, ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, closingState)
val ourSig = bin2signature(Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey))
val anchorTxId = commitment.tx.txIn(0).outPoint.txid // commit tx only has 1 input, which is the anchor
close_channel(ourSig, cmd.fee)
}*/
/*def handle_pkt_close(pkt: close_channel, ourParams: OurChannelParams, theirParams: TheirChannelParams, commitment: Commitment): (Transaction, close_channel_complete) = {
val closingState = commitment.state.adjust_fees(pkt.closeFee * 1000, ourParams.anchorAmount.isDefined)
val finalTx = makeFinalTx(commitment.tx.txIn, ourParams.finalPubKey, theirParams.finalPubKey, closingState)
val ourSig = Transaction.signInput(finalTx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, ourParams.commitPrivKey)
val signedFinalTx = finalTx.updateSigScript(0, sigScript2of2(pkt.sig, ourSig, theirParams.commitPubKey, ourParams.commitPubKey))
(signedFinalTx, close_channel_complete(ourSig))
}*/
/**
*
* @param commitments
* @param ourScriptPubKey
* @param theirScriptPubKey
* @param closeFee bitcoin fee for the final tx
* @return a (final tx, our signature) tuple. The tx is not signed.
*/
def makeFinalTx(commitments: Commitments, ourScriptPubKey: BinaryData, theirScriptPubKey: BinaryData, closeFee: Satoshi): (Transaction, close_signature) = {
val amount_us = Satoshi(commitments.ourCommit.spec.amount_us_msat / 1000)
val amount_them = Satoshi(commitments.theirCommit.spec.amount_us_msat / 1000)
val finalTx = Scripts.makeFinalTx(commitments.ourCommit.publishableTx.txIn, ourScriptPubKey, theirScriptPubKey, amount_us, amount_them, closeFee)
val ourSig = Helpers.sign(commitments.ourParams, commitments.theirParams, commitments.anchorOutput.amount.toLong, finalTx)
(finalTx, close_signature(closeFee.toLong, ourSig))
}
/**
*
* @param commitments
* @param ourScriptPubKey
* @param theirScriptPubKey
* @return a (final tx, our signature) tuple. The tx is not signed. Bitcoin fees will be copied from our
* last commit tx
*/
def makeFinalTx(commitments: Commitments, ourScriptPubKey: BinaryData, theirScriptPubKey: BinaryData): (Transaction, close_signature) = {
val commitFee = commitments.anchorOutput.amount.toLong - commitments.ourCommit.publishableTx.txOut.map(_.amount.toLong).sum
val closeFee = Satoshi(2 * (commitFee / 4))
makeFinalTx(commitments, ourScriptPubKey, theirScriptPubKey, closeFee)
}
}

View file

@ -57,6 +57,8 @@ object Scripts {
}
def pay2pkh(pubKey: BinaryData): Seq[ScriptElt] = OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash160(pubKey)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil
def pay2sh(script: Seq[ScriptElt]): Seq[ScriptElt] = pay2sh(Script.write(script))
def pay2sh(script: BinaryData): Seq[ScriptElt] = OP_HASH160 :: OP_PUSHDATA(hash160(script)) :: OP_EQUAL :: Nil
@ -176,6 +178,15 @@ object Scripts {
// permuteOutputs(tx1)
// }
def applyFees(amount_us: Satoshi, amount_them: Satoshi, fee: Satoshi) = {
val (amount_us1: Satoshi, amount_them1: Satoshi) = (amount_us, amount_them) match {
case (Satoshi(us), Satoshi(them)) if us >= fee.toLong /2 && them >= fee.toLong / 2 => (Satoshi(us - fee.toLong / 2), Satoshi(them - fee.toLong / 2))
case (Satoshi(us), Satoshi(them)) if us < fee.toLong/2 => (Satoshi(0L), Satoshi(Math.max(0L, them - fee.toLong + us)))
case (Satoshi(us), Satoshi(them)) if them < fee.toLong/2 => (Satoshi(Math.max(us - fee.toLong + them, 0L)), Satoshi(0L))
}
(amount_us1, amount_them1)
}
def makeCommitTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): Transaction = {
val redeemScript = redeemSecretOrDelay(ourFinalKey, locktime2long_csv(theirDelay), theirFinalKey, revocationHash: BinaryData)
val htlcs = commitmentSpec.htlcs.filter(_.amountMsat >= 546000)
@ -209,27 +220,28 @@ object Scripts {
}
/**
* This is a simple tx with a multisig input and two pay2sh output
* Create a "final" channel transaction that will be published when the channel is closed
*
* @param inputs inputs to include in the tx. In most cases, there's only one input that points to the output of
* the anchor tx
* @param ourFinalKey our final public key
* @param theirFinalKey their final public key
* @param channelState channel state
* @param ourPubkeyScript our public key script
* @param theirPubkeyScript their public key script
* @param amount_us pay to us
* @param amount_them pay to them
* @return an unsigned "final" tx
*/
// def makeFinalTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, channelState: ChannelState): Transaction = {
// assert(channelState.them.htlcs_received.isEmpty && channelState.us.htlcs_received.isEmpty, s"cannot close a channel with pending htlcs (see rusty's state_types.h line 103)")
//
// permuteOutputs(Transaction(
// version = 2,
// txIn = inputs,
// txOut = Seq(
// TxOut(amount = Satoshi(channelState.them.pay_msat / 1000), publicKeyScript = pay2wpkh(theirFinalKey)),
// TxOut(amount = Satoshi(channelState.us.pay_msat / 1000), publicKeyScript = pay2wpkh(ourFinalKey))
// ),
// lockTime = 0))
// }
def makeFinalTx(inputs: Seq[TxIn], ourPubkeyScript: BinaryData, theirPubkeyScript: BinaryData, amount_us: Satoshi, amount_them: Satoshi, fee: Satoshi): Transaction = {
val (amount_us1: Satoshi, amount_them1: Satoshi) = applyFees(amount_us, amount_them, fee)
permuteOutputs(Transaction(
version = 2,
txIn = inputs,
txOut = Seq(
TxOut(amount = amount_us1, publicKeyScript = ourPubkeyScript),
TxOut(amount = amount_them1, publicKeyScript = theirPubkeyScript)
),
lockTime = 0))
}
def isFunder(o: open_channel): Boolean = o.anch == open_channel.anchor_offer.WILL_CREATE_ANCHOR

View file

@ -4,19 +4,17 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
import akka.testkit.TestProbe
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair._
import lightning.{locktime, update_add_htlc}
import lightning.{locktime, update_add_htlc, update_fulfill_htlc}
import lightning.locktime.Locktime.Blocks
import org.junit.runner.RunWith
import org.scalatest.Ignore
import org.scalatest.junit.JUnitRunner
import scala.collection.Set
import scala.collection.immutable.Set
import scala.concurrent.duration._
/**
* Created by PM on 26/04/2016.
*/
@Ignore
@RunWith(classOf[JUnitRunner])
class NominalChannelSpec extends BaseChannelTestClass {
test("open channel and reach normal state") { case (alice, bob, pipe) =>
@ -62,12 +60,12 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice.stateData match {
case d: DATA_NORMAL =>
val List(update_add_htlc(_, _, h, _, _)) = d.ourChanges.proposed
val List(update_add_htlc(_, _, h, _, _)) = d.commitments.ourChanges.proposed
assert(h == bin2sha256(H))
}
bob.stateData match {
case d: DATA_NORMAL =>
val List(update_add_htlc(_, _, h, _, _)) = d.theirChanges.proposed
val List(update_add_htlc(_, _, h, _, _)) = d.commitments.theirChanges.proposed
assert(h == bin2sha256(H))
}
@ -76,12 +74,12 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice.stateData match {
case d: DATA_NORMAL =>
val htlc = d.theirCommit.spec.htlcs.head
val htlc = d.commitments.theirCommit.spec.htlcs.head
assert(htlc.rHash == bin2sha256(H))
}
bob.stateData match {
case d: DATA_NORMAL =>
val htlc = d.ourCommit.spec.htlcs.head
val htlc = d.commitments.ourCommit.spec.htlcs.head
assert(htlc.rHash == bin2sha256(H))
}
@ -89,36 +87,79 @@ class NominalChannelSpec extends BaseChannelTestClass {
bob ! CMD_SIGN
alice ! CMD_SIGN
Thread.sleep(200)
Thread.sleep(300)
alice.stateData match {
case d: DATA_NORMAL =>
assert(d.ourCommit.spec.htlcs.isEmpty)
assert(d.ourCommit.spec.amount_us_msat == d.ourCommit.spec.initial_amount_us_msat - 60000000)
assert(d.ourCommit.spec.amount_them_msat == d.ourCommit.spec.initial_amount_them_msat + 60000000)
assert(d.commitments.ourCommit.spec.htlcs.isEmpty)
assert(d.commitments.ourCommit.spec.amount_us_msat == d.commitments.ourCommit.spec.initial_amount_us_msat - 60000000)
assert(d.commitments.ourCommit.spec.amount_them_msat == d.commitments.ourCommit.spec.initial_amount_them_msat + 60000000)
}
bob.stateData match {
case d: DATA_NORMAL =>
assert(d.ourCommit.spec.htlcs.isEmpty)
assert(d.ourCommit.spec.amount_us_msat == d.ourCommit.spec.initial_amount_us_msat + 60000000)
assert(d.ourCommit.spec.amount_them_msat == d.ourCommit.spec.initial_amount_them_msat - 60000000)
assert(d.commitments.ourCommit.spec.htlcs.isEmpty)
assert(d.commitments.ourCommit.spec.amount_us_msat == d.commitments.ourCommit.spec.initial_amount_us_msat + 60000000)
assert(d.commitments.ourCommit.spec.amount_them_msat == d.commitments.ourCommit.spec.initial_amount_them_msat - 60000000)
}
}
}
test("close channel starting with no HTLC") { case (alice, bob, pipe) =>
// pipe !(alice, bob) // this starts the communication between alice and bob
//
// within(30 seconds) {
//
// awaitCond(alice.stateName == NORMAL)
// awaitCond(bob.stateName == NORMAL)
//
// alice ! CMD_CLOSE(None)
//
// awaitCond(alice.stateName == CLOSING)
// awaitCond(bob.stateName == CLOSING)
// }
pipe !(alice, bob) // this starts the communication between alice and bob
within(30 seconds) {
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
alice ! CMD_CLOSE(None)
awaitCond(alice.stateName == CLOSING)
awaitCond(bob.stateName == CLOSING)
}
}
test("close channel with pending htlcs") { case (alice, bob, pipe) =>
within(30 seconds) {
pipe !(alice, bob) // this starts the communication between alice and bob
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
val monitorA = TestProbe()
alice ! SubscribeTransitionCallBack(monitorA.ref)
val CurrentState(_, NORMAL) = monitorA.expectMsgClass(classOf[CurrentState[_]])
val monitorB = TestProbe()
bob ! SubscribeTransitionCallBack(monitorB.ref)
val CurrentState(_, NORMAL) = monitorB.expectMsgClass(classOf[CurrentState[_]])
def expectTransition(monitor: TestProbe, from: State, to: State): Unit = {
val Transition(_, from, to) = monitor.expectMsgClass(classOf[Transition[_]])
}
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(4)))
alice ! CMD_SIGN
bob ! CMD_SIGN
alice ! CMD_CLOSE(None)
expectTransition(monitorA, NORMAL, CLEARING)
expectTransition(monitorB, NORMAL, CLEARING)
bob ! CMD_FULFILL_HTLC(1, R)
bob ! CMD_SIGN
Thread.sleep(100)
alice ! CMD_SIGN
expectTransition(monitorA, CLEARING, NEGOCIATING)
expectTransition(monitorB, CLEARING, NEGOCIATING)
expectTransition(monitorA, NEGOCIATING, CLOSING)
expectTransition(monitorB, NEGOCIATING, CLOSING)
expectTransition(monitorA, CLOSING, CLOSED)
expectTransition(monitorB, CLOSING, CLOSED)
}
}
}

View file

@ -129,21 +129,22 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging
exec(script.drop(1), a, b)
case d: DATA_NORMAL if script.head.endsWith(":dump") =>
def rtrim(s: String) = s.replaceAll("\\s+$", "")
import d.commitments._
val l = List(
"LOCAL COMMITS:",
s" Commit ${d.ourCommit.index}:",
s" Offered htlcs: ${d.ourCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Received htlcs: ${d.ourCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Balance us: ${d.ourCommit.spec.amount_us_msat}",
s" Balance them: ${d.ourCommit.spec.amount_them_msat}",
s" Fee rate: ${d.ourCommit.spec.feeRate}",
s" Commit ${d.commitments.ourCommit.index}:",
s" Offered htlcs: ${ourCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Received htlcs: ${ourCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Balance us: ${ourCommit.spec.amount_us_msat}",
s" Balance them: ${ourCommit.spec.amount_them_msat}",
s" Fee rate: ${ourCommit.spec.feeRate}",
"REMOTE COMMITS:",
s" Commit ${d.theirCommit.index}:",
s" Offered htlcs: ${d.theirCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Received htlcs: ${d.theirCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Balance us: ${d.theirCommit.spec.amount_us_msat}",
s" Balance them: ${d.theirCommit.spec.amount_them_msat}",
s" Fee rate: ${d.theirCommit.spec.feeRate}")
s" Commit ${theirCommit.index}:",
s" Offered htlcs: ${theirCommit.spec.htlcs.filter(_.direction == OUT).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Received htlcs: ${theirCommit.spec.htlcs.filter(_.direction == IN).map(h => (h.id, h.amountMsat)).mkString(" ")}",
s" Balance us: ${theirCommit.spec.amount_us_msat}",
s" Balance them: ${theirCommit.spec.amount_them_msat}",
s" Fee rate: ${theirCommit.spec.feeRate}")
.foreach(s => {
fout.write(rtrim(s))
fout.newLine()