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:
commit
673cc61d6b
11 changed files with 600 additions and 456 deletions
62
TESTING.md
62
TESTING.md
|
@ -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 we’ll use port 50000)
|
||||
Start lightningd (here we’ll 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
|
||||
We’ll use the following values for R and H:
|
||||
|
@ -53,19 +86,26 @@ You’ll 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
23
eclair-demo/eclair-cli
Executable 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
|
||||
|
|
@ -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",
|
||||
|
|
|
@ -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))}")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue