1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-19 09:54:02 +01:00

Wip uniclose txdb (#15)

* implement a basic tx db that stores how to spend revoked transactions

it's a simple db that stores spending tx indexed by their parent id

* bumped akka version to 2.4.8

* fixed wrong data type in CLEARING and NEGOTIATING handlers

* added a few common handlers

* moved sendAdd/receiveAdd logic to Commitments.scala

* moved htlc index to Commitments class

* cleaned up sig handlers

* removed unused variable

* storing their commit's txid so that we can identify spending tx

* added a log to unimplemented function 'handleTheirSpentCurrent'

* changed ascii art helpers->handlers

* fix misleading comments and code in Channel

* removed unused imports

* added test showing bug when two htlcs share the same r

* added a failing test showing that punish tx output isn't p2wpkh

* fix failing test

* use P2WPKH instead of P2PKH in our "punishment" tx

* fix failing test

* added helper methods in test

* fixed test
This commit is contained in:
Pierre-Marie Padiou 2016-07-20 17:39:19 +02:00 committed by GitHub
parent 90811d1d5a
commit 9d419b9d8a
11 changed files with 555 additions and 492 deletions

View File

@ -1,11 +1,11 @@
package fr.acinq.eclair.channel
import akka.actor.{ActorRef, FSM, LoggingFSM, Props}
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers._
import fr.acinq.eclair.channel.TypeDefs.Change
import fr.acinq.eclair.crypto.ShaChain
import lightning._
import lightning.open_channel.anchor_offer.{WILL_CREATE_ANCHOR, WONT_CREATE_ANCHOR}
@ -45,6 +45,32 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
888 888 Y8888 888 888
8888888 888 Y888 8888888 888
*/
/*
FUNDER FUNDEE
| open_channel open_channel |
|--------------- ---------------|
OPEN_WAIT_FOR_OPEN_WITHANCHOR| \/ |OPEN_WAIT_FOR_OPEN_NOANCHOR
| /\ |
|<-------------- -------------->|
| |OPEN_WAIT_FOR_ANCHOR
| open_anchor |
|--------------- |
OPEN_WAIT_FOR_COMMIT_SIG| \ |
| --------------->|
| open_commit_sig |
| ----------------|
| / |OPEN_WAITING_THEIRANCHOR
|<-------------- |
OPEN_WAITING_OURANCHOR| |
| |
| open_complete openu_complete |
|--------------- ---------------|
OPEN_WAIT_FOR_COMPLETE_OURANCHOR| \/ |OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR
| /\ |
|<-------------- -------------->|
NORMAL| |NORMAL
*/
when(OPEN_WAIT_FOR_OPEN_NOANCHOR)(handleExceptions {
case Event(open_channel(delay, theirRevocationHash, theirNextRevocationHash, commitKey, finalKey, WILL_CREATE_ANCHOR, minDepth, initialFeeRate), DATA_OPEN_WAIT_FOR_OPEN(ourParams)) =>
val theirParams = TheirChannelParams(delay, commitKey, finalKey, minDepth, initialFeeRate)
@ -69,7 +95,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
val amount = anchorTx.txOut(anchorOutputIndex).amount.toLong
val theirSpec = CommitmentSpec(Set.empty[Htlc], feeRate = theirParams.initialFeeRate, initial_amount_us_msat = 0, initial_amount_them_msat = amount * 1000, amount_us_msat = 0, amount_them_msat = amount * 1000)
them ! open_anchor(anchorTx.hash, anchorOutputIndex, amount)
goto(OPEN_WAIT_FOR_COMMIT_SIG) using DATA_OPEN_WAIT_FOR_COMMIT_SIG(ourParams, theirParams, anchorTx, anchorOutputIndex, TheirCommit(0, theirSpec, theirRevocationHash), theirNextRevocationHash)
goto(OPEN_WAIT_FOR_COMMIT_SIG) using DATA_OPEN_WAIT_FOR_COMMIT_SIG(ourParams, theirParams, anchorTx, anchorOutputIndex, TheirCommit(0, theirSpec, BinaryData(""), theirRevocationHash), theirNextRevocationHash)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
@ -88,20 +114,24 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
val ourSpec = CommitmentSpec(Set.empty[Htlc], feeRate = ourParams.initialFeeRate, initial_amount_them_msat = anchorAmount * 1000, initial_amount_us_msat = 0, amount_them_msat = anchorAmount * 1000, amount_us_msat = 0)
val theirSpec = CommitmentSpec(Set.empty[Htlc], feeRate = theirParams.initialFeeRate, initial_amount_them_msat = 0, initial_amount_us_msat = anchorAmount * 1000, amount_them_msat = 0, amount_us_msat = anchorAmount * 1000)
// 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 ourTx = makeOurTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, ourRevocationHash, ourSpec)
// build and sign their commit tx
val theirTx = makeTheirTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, theirRevocationHash, theirSpec)
log.info(s"signing their tx: $theirTx")
val ourSigForThem = sign(ourParams, theirParams, Satoshi(anchorAmount), theirTx)
them ! open_commit_sig(ourSigForThem)
// watch the anchor
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. We won't lose money since they are funding the chanel
val ourRevocationHash = Helpers.revocationHash(ourParams.shaSeed, 0)
val ourTx = makeOurTx(ourParams, theirParams, TxIn(OutPoint(anchorTxHash, anchorOutputIndex), Array.emptyByteArray, 0xffffffffL) :: Nil, ourRevocationHash, ourSpec)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirRevocationHash),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput, ShaChain.init)
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, theirTx.txid, theirRevocationHash),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
Right(theirNextRevocationHash), anchorOutput, ShaChain.init, new BasicTxDb)
goto(OPEN_WAITING_THEIRANCHOR) using DATA_OPEN_WAITING(commitments, None)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
@ -135,8 +165,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
blockchain ! Publish(anchorTx)
val commitments = Commitments(ourParams, theirParams,
OurCommit(0, ourSpec, signedTx), theirCommitment,
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(theirNextRevocationHash), anchorOutput, ShaChain.init)
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
Right(theirNextRevocationHash), anchorOutput, ShaChain.init, new BasicTxDb)
goto(OPEN_WAITING_OURANCHOR) using DATA_OPEN_WAITING(commitments, None)
}
@ -157,18 +187,20 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
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(commitments, 0, None)
goto(OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR) using DATA_NORMAL(commitments, None)
case Event(BITCOIN_ANCHOR_TIMEOUT, _) =>
them ! error(Some("Anchor timed out"))
goto(CLOSED)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) =>
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_OPEN_WAITING) if tx.txid == d.commitments.theirCommit.txid =>
// they are funding the anchor, we have nothing at stake
log.warning(s"their anchor ${d.commitments.anchorId} was spent, sending error and closing")
them ! error(Some(s"your anchor ${d.commitments.anchorId} was spent"))
goto(CLOSED)
case Event((BITCOIN_ANCHOR_SPENT, _), d: HasCommitments) => handleInformationLeak(d)
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e@error(problem), _) =>
@ -186,18 +218,13 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
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(commitments, 0, None)
goto(OPEN_WAIT_FOR_COMPLETE_OURANCHOR) using DATA_NORMAL(commitments, None)
case Event(BITCOIN_ANCHOR_TIMEOUT, _) =>
them ! error(Some("Anchor timed out"))
goto(CLOSED)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) =>
// this is never supposed to happen !!
log.error(s"our anchor ${d.commitments.anchorId} was spent !!")
them ! error(Some("Anchor has been spent"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
goto(ERR_INFORMATION_LEAK)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_OPEN_WAITING) => handleInformationLeak(d)
case Event(cmd: CMD_CLOSE, d: DATA_OPEN_WAITING) =>
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
@ -216,7 +243,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) =>
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.theirCommit.txid =>
// they are funding the anchor, we have nothing at stake
log.warning(s"their anchor ${d.commitments.anchorId} was spent, sending error and closing")
them ! error(Some(s"your anchor ${d.commitments.anchorId} was spent"))
@ -234,23 +261,14 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
Register.create_alias(theirNodeId, d.commitments.anchorId)
goto(NORMAL)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) =>
// this is never supposed to happen !!
log.error(s"our anchor ${d.commitments.anchorId} was spent while we were waiting for their open_complete message !!")
them ! error(Some("Anchor has been spent"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
goto(ERR_INFORMATION_LEAK)
case Event((BITCOIN_ANCHOR_SPENT, _), d: DATA_NORMAL) => handleInformationLeak(d)
case Event(cmd: CMD_CLOSE, d: DATA_NORMAL) =>
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Event(e@error(problem), d: DATA_NORMAL) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Event(e@error(problem), d: DATA_NORMAL) => handleError(e, d)
})
@ -266,112 +284,46 @@ 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(commitments, htlcIdx, _)) =>
// TODO : this should probably be done in Commitments.scala
// our available funds as seen by them, including all pending changes
val reduced = reduce(commitments.theirCommit.spec, commitments.theirChanges.acked, commitments.ourChanges.acked ++ commitments.ourChanges.signed ++ commitments.ourChanges.proposed)
// the pending htlcs that we sent to them (seen as IN from their pov) have already been deduced from our balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (amount > available) {
sender ! s"insufficient funds (available=$available msat)"
stay
} else {
// TODO: nodeIds are ignored
val id: Long = id_opt.getOrElse(htlcIdx + 1)
val steps = route(route_step(0, next = route_step.Next.End(true)) :: Nil)
val htlc = update_add_htlc(id, amount, rHash, expiry, routing(ByteString.copyFrom(steps.toByteArray)))
them ! htlc
sender ! "ok"
stay using d.copy(htlcIdx = htlc.id, commitments = commitments.addOurProposal(htlc))
case Event(c@CMD_ADD_HTLC(amount, rHash, expiry, nodeIds, origin, id_opt), d@DATA_NORMAL(commitments, _)) =>
Try(Commitments.sendAdd(commitments, c)) match {
case Success((commitments1, add)) => handleCommandSuccess(sender, add, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(htlc@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _, _)) =>
// TODO : this should probably be done in Commitments.scala
// their available funds as seen by us, including all pending changes
val reduced = reduce(commitments.ourCommit.spec, commitments.ourChanges.acked, commitments.theirChanges.acked ++ commitments.theirChanges.proposed)
// the pending htlcs that they sent to us (seen as IN from our pov) have already been deduced from their balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (amount > available) {
log.error("they sent an htlc but had insufficient funds")
them ! error(Some("Insufficient funds"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
} else {
// TODO: nodeIds are ignored
stay using d.copy(commitments = commitments.addTheirProposal(htlc))
case Event(add@update_add_htlc(htlcId, amount, rHash, expiry, nodeIds), d@DATA_NORMAL(commitments, _)) =>
Try(Commitments.receiveAdd(commitments, add)) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
Try(Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))) match {
case Success((commitments1, fullfill)) =>
them ! fullfill
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
sender ! cause.getMessage
stay
case Event(c@CMD_FULFILL_HTLC(id, r), d: DATA_NORMAL) =>
Try(Commitments.sendFulfill(d.commitments, c)) match {
case Success((commitments1, fulfill)) => handleCommandSuccess(sender, fulfill, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_NORMAL) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
Try(Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))) match {
case Success((commitments1, fail)) =>
them ! fail
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
sender ! cause.getMessage
stay
case Event(c@CMD_FAIL_HTLC(id, reason), d: DATA_NORMAL) =>
Try(Commitments.sendFail(d.commitments, c)) match {
case Success((commitments1, fail)) => handleCommandSuccess(sender, fail, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fail@update_fail_htlc(id, reason), d: DATA_NORMAL) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_SIGN, d: DATA_NORMAL) =>
if (d.commitments.theirNextCommitInfo.isLeft) {
sender ! "cannot sign until next revocation hash is received"
stay
} /*else if (d.commitments.ourChanges.proposed.isEmpty) {
//TODO : check this
sender ! "cannot sign when there are no changes"
stay
}*/
else {
Try(Commitments.sendCommit(d.commitments)) match {
case Success((commitments1, commit)) =>
them ! commit
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
Try(Commitments.sendCommit(d.commitments)) match {
case Success((commitments1, commit)) => handleCommandSuccess(sender, commit, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(msg@update_commit(theirSig), d: DATA_NORMAL) =>
@ -379,12 +331,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
case Success((commitments1, revocation)) =>
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
@ -393,12 +340,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_CLOSE(scriptPubKeyOpt), d: DATA_NORMAL) =>
@ -415,7 +357,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
stay using d.copy(ourClearing = Some(ourCloseClearing))
}
case Event(theirClearing@close_clearing(theirScriptPubKey), d@DATA_NORMAL(commitments, _, ourClearingOpt)) =>
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)}")
@ -424,31 +366,18 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
c
}
if (commitments.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
val (_, ourCloseSig) = makeFinalTx(commitments, ourClearing.scriptPubkey, theirScriptPubKey)
them ! ourCloseSig
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments, ourClearing, theirClearing, ourCloseSig)
} else {
goto(CLEARING) using DATA_CLEARING(commitments, d.htlcIdx, ourClearing, theirClearing)
goto(CLEARING) using DATA_CLEARING(commitments, ourClearing, theirClearing)
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
log.warning(s"anchor spent in ${tx.txid}")
them ! error(Some("Anchor has been spent"))
Helpers.claimTheirRevokedCommit(tx, d.commitments) match {
case Some(spendingTx) =>
blockchain ! Publish(spendingTx)
blockchain ! WatchConfirmed(self, spendingTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
case None =>
// TODO: this is definitely not right!
stay()
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.theirCommit.txid => handleTheirSpentCurrent(tx, d)
case Event(e@error(problem), d: DATA_NORMAL) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) => handleTheirSpentOther(tx, d)
case Event(e@error(problem), d: DATA_NORMAL) => handleError(e, d)
}
@ -464,124 +393,67 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
*/
when(CLEARING) {
case Event(CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
Try(Commitments.sendFulfill(d.commitments, CMD_FULFILL_HTLC(id, r))) match {
case Success((commitments1, fullfill)) =>
them ! fullfill
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
sender ! cause.getMessage
stay
case Event(c@CMD_FULFILL_HTLC(id, r), d: DATA_CLEARING) =>
Try(Commitments.sendFulfill(d.commitments, c)) match {
case Success((commitments1, fulfill)) => handleCommandSuccess(sender, fulfill, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fulfill@update_fulfill_htlc(id, r), d: DATA_CLEARING) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
Try(Commitments.sendFail(d.commitments, CMD_FAIL_HTLC(id, reason))) match {
case Success((commitments1, fail)) =>
them ! fail
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
sender ! cause.getMessage
stay
case Event(c@CMD_FAIL_HTLC(id, reason), d: DATA_CLEARING) =>
Try(Commitments.sendFail(d.commitments, c)) match {
case Success((commitments1, fail)) => handleCommandSuccess(sender, fail, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(fail@update_fail_htlc(id, reason), d: DATA_CLEARING) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(CMD_SIGN, d: DATA_CLEARING) =>
if (d.commitments.theirNextCommitInfo.isLeft) {
sender ! "cannot sign until next revocation hash is received"
stay
} /*else if (d.commitments.ourChanges.proposed.isEmpty) {
//TODO : check this
sender ! "cannot sign when there are no changes"
stay
}*/
else {
Try(Commitments.sendCommit(d.commitments)) match {
case Success((commitments1, commit)) =>
them ! commit
sender ! "ok"
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
Try(Commitments.sendCommit(d.commitments)) match {
case Success((commitments1, commit)) => handleCommandSuccess(sender, commit, d.copy(commitments = commitments1))
case Failure(cause) => handleCommandError(sender, cause)
}
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, _, ourClearing, theirClearing)) =>
case Event(msg@update_commit(theirSig), d@DATA_CLEARING(commitments, ourClearing, theirClearing)) =>
Try(Commitments.receiveCommit(d.commitments, msg)) match {
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
them ! revocation
val (_, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, ourClearing, theirClearing, ourCloseSig)
case Success((commitments1, revocation)) =>
them ! revocation
if (commitments1.hasNoPendingHtlcs) {
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
} else {
stay using d.copy(commitments = commitments1)
}
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, _, ourClearing, theirClearing)) =>
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, ourClearing, theirClearing)) =>
// we received a revocation because we sent a signature
// => all our changes have been acked
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
val (finalTx, ourCloseSig) = makeFinalTx(commitments1, ourClearing.scriptPubkey, theirClearing.scriptPubkey)
them ! ourCloseSig
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, d.htlcIdx, ourClearing, theirClearing, ourCloseSig)
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, ourClearing, theirClearing, ourCloseSig)
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) =>
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Failure(cause) => handleUnicloseError(cause, d)
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
// TODO : not implemented
stay
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) if tx.txid == d.commitments.theirCommit.txid => handleTheirSpentCurrent(tx, d)
case Event(e@error(problem), d: DATA_CLEARING) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) => handleTheirSpentOther(tx, d)
case Event(e@error(problem), d: DATA_CLEARING) => handleError(e, d)
}
when(NEGOTIATING) {
@ -618,16 +490,14 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
throw new RuntimeException("cannot verify their close signature", cause)
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) =>
// TODO : not implemented
// seing the anchor being spent here could be normal
stay
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NEGOTIATING) if tx.txid == d.commitments.theirCommit.txid =>
// TODO : this may be normal
handleTheirSpentCurrent(tx, d)
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NEGOTIATING) => handleTheirSpentOther(tx, d)
case Event(e@error(problem), d: DATA_NEGOTIATING) => handleError(e, d)
case Event(e@error(problem), d: DATA_NEGOTIATING) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
when(CLOSING) {
@ -681,6 +551,74 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, val params: OurChann
}
/*
888 888 d8888 888b 888 8888888b. 888 8888888888 8888888b. .d8888b.
888 888 d88888 8888b 888 888 "Y88b 888 888 888 Y88b d88P Y88b
888 888 d88P888 88888b 888 888 888 888 888 888 888 Y88b.
8888888888 d88P 888 888Y88b 888 888 888 888 8888888 888 d88P "Y888b.
888 888 d88P 888 888 Y88b888 888 888 888 888 8888888P" "Y88b.
888 888 d88P 888 888 Y88888 888 888 888 888 888 T88b "888
888 888 d8888888888 888 Y8888 888 .d88P 888 888 888 T88b Y88b d88P
888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P"
*/
def handleCommandSuccess(sender: ActorRef, change: Change, newData: Data) = {
them ! change
sender ! "ok"
stay using newData
}
def handleCommandError(sender: ActorRef, cause: Throwable) = {
log.error(cause, "")
sender ! cause.getMessage
stay
}
def handleUnicloseError(cause: Throwable, d: HasCommitments) = {
log.error(cause, "")
them ! error(Some(cause.getMessage))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
def handleTheirSpentCurrent(tx: Transaction, d: HasCommitments) = {
log.warning(s"they published their current commit in txid=${tx.txid}")
// TODO
???
}
def handleTheirSpentOther(tx: Transaction, d: HasCommitments) = {
log.warning(s"anchor spent in txid=${tx.txid}")
d.commitments.txDb.get(tx.txid) match {
case Some(spendingTx) =>
log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the punishment tx")
them ! error(Some("Anchor has been spent"))
blockchain ! Publish(spendingTx)
blockchain ! WatchConfirmed(self, spendingTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
case None =>
// the published tx was neither their current commitment nor a revoked one
log.error(s"couldn't identify txid=${tx.txid}!")
goto(ERR_INFORMATION_LEAK)
}
}
def handleError(e: error, d: HasCommitments) = {
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
blockchain ! WatchConfirmed(self, d.commitments.ourCommit.publishableTx.txid, d.commitments.ourParams.minDepth, BITCOIN_CLOSE_DONE)
goto(CLOSING) using DATA_CLOSING(d.commitments, ourCommitPublished = Some(d.commitments.ourCommit.publishableTx))
}
def handleInformationLeak(d: HasCommitments) = {
// this is never supposed to happen !!
log.error(s"our anchor ${d.commitments.anchorId} was spent !!")
them ! error(Some("Anchor has been spent"))
blockchain ! Publish(d.commitments.ourCommit.publishableTx)
goto(ERR_INFORMATION_LEAK)
}
/**
* This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel
*/

View File

@ -1,8 +1,6 @@
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 fr.acinq.bitcoin.{BinaryData, Crypto, Transaction}
import lightning._
/**
@ -131,16 +129,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
}
object TypeDefs {
type Change = GeneratedMessage
}
import TypeDefs._
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change])
case class TheirChanges(proposed: List[Change], acked: List[Change])
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 ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
trait HasCommitments {
@ -152,11 +140,11 @@ final case class DATA_OPEN_WITH_ANCHOR_WAIT_FOR_ANCHOR(ourParams: OurChannelPara
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, deferred: Option[open_complete]) extends Data with HasCommitments
final case class DATA_NORMAL (commitments: Commitments, htlcIdx: Long,
final case class DATA_NORMAL (commitments: Commitments,
ourClearing: Option[close_clearing]) extends Data with HasCommitments
final case class DATA_CLEARING (commitments: Commitments, htlcIdx: Long,
final case class DATA_CLEARING (commitments: Commitments,
ourClearing: close_clearing, theirClearing: close_clearing) extends Data with HasCommitments
final case class DATA_NEGOTIATING (commitments: Commitments, htlcIdx: Long,
final case class DATA_NEGOTIATING (commitments: Commitments,
ourClearing: close_clearing, theirClearing: close_clearing, ourSignature: close_signature) extends Data with HasCommitments
final case class DATA_CLOSING (commitments: Commitments,
ourSignature: Option[close_signature] = None,

View File

@ -1,12 +1,43 @@
package fr.acinq.eclair.channel
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Transaction, TxOut}
import com.trueaccord.scalapb.GeneratedMessage
import fr.acinq.bitcoin.{BinaryData, Crypto, ScriptFlags, Transaction, TxOut}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.Scripts.TxTemplate
import fr.acinq.eclair.channel.TypeDefs.Change
import fr.acinq.eclair.crypto.ShaChain
import lightning._
trait TxDb {
def add(parentId: BinaryData, spending: Transaction): Unit
def get(parentId: BinaryData): Option[Transaction]
}
class BasicTxDb extends TxDb {
val db = collection.mutable.HashMap.empty[BinaryData, Transaction]
override def add(parentId: BinaryData, spending: Transaction): Unit = {
db += parentId -> spending
}
override def get(parentId: BinaryData): Option[Transaction] = db.get(parentId)
}
// @formatter:off
object TypeDefs {
type Change = GeneratedMessage
}
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change])
case class TheirChanges(proposed: List[Change], acked: List[Change])
case class Changes(ourChanges: OurChanges, theirChanges: TheirChanges)
case class OurCommit(index: Long, spec: CommitmentSpec, publishableTx: Transaction)
case class TheirCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, theirRevocationHash: sha256_hash)
// @formatter:on
/**
* about theirNextCommitInfo:
* we either:
@ -18,8 +49,9 @@ import lightning._
case class Commitments(ourParams: OurChannelParams, theirParams: TheirChannelParams,
ourCommit: OurCommit, theirCommit: TheirCommit,
ourChanges: OurChanges, theirChanges: TheirChanges,
ourCurrentHtlcId: Long,
theirNextCommitInfo: Either[TheirCommit, BinaryData],
anchorOutput: TxOut, theirPreimages: ShaChain) {
anchorOutput: TxOut, theirPreimages: ShaChain, txDb: TxDb) {
def anchorId: BinaryData = {
assert(ourCommit.publishableTx.txIn.size == 1, "commitment tx should only have one input")
ourCommit.publishableTx.txIn(0).outPoint.hash
@ -40,12 +72,42 @@ object 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))
private 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 =
private def addTheirProposal(commitments: Commitments, proposal: Change): Commitments =
commitments.copy(theirChanges = commitments.theirChanges.copy(proposed = commitments.theirChanges.proposed :+ proposal))
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC): (Commitments, update_add_htlc) = {
// our available funds as seen by them, including all pending changes
val reduced = Helpers.reduce(commitments.theirCommit.spec, commitments.theirChanges.acked, commitments.ourChanges.acked ++ commitments.ourChanges.signed ++ commitments.ourChanges.proposed)
// the pending htlcs that we sent to them (seen as IN from their pov) have already been deduced from our balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (cmd.amountMsat > available) {
throw new RuntimeException(s"insufficient funds (available=$available msat)")
} else {
// TODO: nodeIds are ignored
val id = cmd.id.getOrElse(commitments.ourCurrentHtlcId + 1)
val steps = route(route_step(0, next = route_step.Next.End(true)) :: Nil)
val add = update_add_htlc(id, cmd.amountMsat, cmd.rHash, cmd.expiry, routing(ByteString.copyFrom(steps.toByteArray)))
val commitments1 = addOurProposal(commitments, add).copy(ourCurrentHtlcId = id)
(commitments1, add)
}
}
def receiveAdd(commitments: Commitments, add: update_add_htlc): Commitments = {
// their available funds as seen by us, including all pending changes
val reduced = Helpers.reduce(commitments.ourCommit.spec, commitments.ourChanges.acked, commitments.theirChanges.acked ++ commitments.theirChanges.proposed)
// the pending htlcs that they sent to us (seen as IN from our pov) have already been deduced from their balance
val available = reduced.amount_them_msat + reduced.htlcs.filter(_.direction == OUT).map(-_.amountMsat).sum
if (add.amountMsat > available) {
throw new RuntimeException("Insufficient funds")
} else {
// TODO: nodeIds are ignored
addTheirProposal(commitments, add)
}
}
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)) =>
@ -84,21 +146,23 @@ object Commitments {
}
def sendCommit(commitments: Commitments): (Commitments, update_commit) = {
// TODO : check empty changes
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 theirTxTemplate = Helpers.makeTheirTxTemplate(ourParams, theirParams, ourCommit.publishableTx.txIn, theirNextRevocationHash, spec)
val theirTx = theirTxTemplate.makeTx
val ourSig = Helpers.sign(ourParams, theirParams, anchorOutput.amount, theirTx)
val commit = update_commit(ourSig)
val commitments1 = commitments.copy(
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, theirNextRevocationHash)),
theirNextCommitInfo = Left(TheirCommit(theirCommit.index + 1, spec, theirTx.txid, 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")
throw new RuntimeException("cannot sign until next revocation hash is received")
}
}
@ -145,6 +209,13 @@ object Commitments {
case Left(theirNextCommit) if BinaryData(Crypto.sha256(revocation.revocationPreimage)) != BinaryData(theirCommit.theirRevocationHash) =>
throw new RuntimeException("invalid preimage")
case Left(theirNextCommit) =>
// this is their revoked commit tx
val theirTxTemplate = Helpers.makeTheirTxTemplate(ourParams, theirParams, ourCommit.publishableTx.txIn, theirCommit.theirRevocationHash, theirCommit.spec)
val theirTx = theirTxTemplate.makeTx
val punishTx = Helpers.claimRevokedCommitTx(theirTxTemplate, revocation.revocationPreimage, ourParams.finalPrivKey)
Transaction.correctlySpends(punishTx, Seq(theirTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
txDb.add(theirTx.txid, punishTx)
commitments.copy(
ourChanges = ourChanges.copy(signed = Nil, acked = ourChanges.acked ++ ourChanges.signed),
theirCommit = theirNextCommit,
@ -154,6 +225,17 @@ object Commitments {
throw new RuntimeException("received unexpected update_revocation message")
}
}
def makeTheirTxTemplate(commitments: Commitments): TxTemplate = {
commitments.theirNextCommitInfo match {
case Left(theirNextCommit) =>
Helpers.makeTheirTxTemplate(commitments.ourParams, commitments.theirParams, commitments.ourCommit.publishableTx.txIn,
theirNextCommit.theirRevocationHash, theirNextCommit.spec)
case Right(revocationHash) =>
Helpers.makeTheirTxTemplate(commitments.ourParams, commitments.theirParams, commitments.ourCommit.publishableTx.txIn,
commitments.theirCommit.theirRevocationHash, commitments.theirCommit.spec)
}
}
}

View File

@ -71,8 +71,11 @@ object Helpers {
def makeOurTx(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], ourRevocationHash: sha256_hash, spec: CommitmentSpec): Transaction =
makeCommitTx(inputs, ourParams.finalPubKey, theirParams.finalPubKey, ourParams.delay, ourRevocationHash, spec)
def makeTheirTxTemplate(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], theirRevocationHash: sha256_hash, spec: CommitmentSpec): TxTemplate =
makeCommitTxTemplate(inputs, theirParams.finalPubKey, ourParams.finalPubKey, theirParams.delay, theirRevocationHash, spec)
def makeTheirTx(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], theirRevocationHash: sha256_hash, spec: CommitmentSpec): Transaction =
makeCommitTx(inputs, theirParams.finalPubKey, ourParams.finalPubKey, theirParams.delay, theirRevocationHash, spec)
makeTheirTxTemplate(ourParams, theirParams, inputs, theirRevocationHash, spec).makeTx
def sign(ourParams: OurChannelParams, theirParams: TheirChannelParams, anchorAmount: Satoshi, tx: Transaction): signature =
bin2signature(Transaction.signInput(tx, 0, multiSig2of2(ourParams.commitPubKey, theirParams.commitPubKey), SIGHASH_ALL, anchorAmount, 1, ourParams.commitPrivKey))
@ -209,4 +212,51 @@ object Helpers {
tx1
}
}
/**
* claim a revoked commit tx
* @param theirTxTemplate revoked commit tx template
* @param revocationPreimage revocation preimage
* @param privateKey private key to send the claimed funds to
* @return a signed transaction that spends the revoked commit tx
*/
def claimRevokedCommitTx(theirTxTemplate: TxTemplate, revocationPreimage: BinaryData, privateKey: BinaryData): Transaction = {
val theirTx = theirTxTemplate.makeTx
val outputs = collection.mutable.ListBuffer.empty[TxOut]
def findOutputIndex(output: TxOut): Option[Int] = {
for (i <- 0 until theirTx.txOut.length) {
if (theirTx.txOut(i) == output) return Some(i)
}
None
}
// first, find out how much we can claim
val outputsToClaim = (theirTxTemplate.ourOutput.toSeq ++ theirTxTemplate.htlcReceived.toSeq ++ theirTxTemplate.htlcSent.toSeq).filter(o => findOutputIndex(o.txOut).isDefined)
val totalAmount = outputsToClaim.map(_.amount).sum
// create a tx that sends everything to our private key
val tx = Transaction(version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(totalAmount, pay2wpkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
witness = Seq.empty[ScriptWitness],
lockTime = 0)
// create tx inputs that spend each output that we can spend
val inputs = outputsToClaim.map(outputTemplate => {
val index = findOutputIndex(outputTemplate.txOut).get
TxIn(OutPoint(theirTx, index), signatureScript = BinaryData.empty, sequence = 0xffffffffL)
})
assert(inputs.length == outputsToClaim.length)
// and sign them
val tx1 = tx.copy(txIn = inputs)
val witnesses = for(i <- 0 until tx1.txIn.length) yield {
val sig = Transaction.signInput(tx1, i, outputsToClaim(i).redeemScript, SIGHASH_ALL, outputsToClaim(i).amount, 1, privateKey)
val witness = ScriptWitness(sig :: revocationPreimage :: outputsToClaim(i).redeemScript :: Nil)
witness
}
tx1.copy(witness = witnesses)
}
}

View File

@ -166,9 +166,45 @@ object Scripts {
(amount_us1, amount_them1)
}
def makeCommitTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): Transaction = {
sealed trait OutputTemplate {
def amount: Satoshi
def txOut: TxOut
// this is the actual script that must be used to claim this output
def redeemScript: BinaryData
}
case class P2WSH(amount: Satoshi, script: BinaryData) extends OutputTemplate {
override def txOut: TxOut = TxOut(amount, pay2wsh(script))
override def redeemScript = script
}
object P2WSH {
def apply(amount: Satoshi, script: Seq[ScriptElt]): P2WSH = new P2WSH(amount, Script.write(script))
}
case class P2WPKH(amount: Satoshi, publicKey: BinaryData) extends OutputTemplate {
override def txOut: TxOut = TxOut(amount, pay2wpkh(publicKey))
override def redeemScript = Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Crypto.hash160(publicKey)) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
}
case class TxTemplate(inputs: Seq[TxIn], ourOutput: Option[OutputTemplate], theirOutput: Option[OutputTemplate], htlcSent: Seq[OutputTemplate], htlcReceived: Seq[OutputTemplate]) {
def makeTx: Transaction = {
val outputs = ourOutput.toSeq ++ theirOutput.toSeq ++ htlcSent ++ htlcReceived
val tx = Transaction(
version = 2,
txIn = inputs,
txOut = outputs.map(_.txOut),
lockTime = 0
)
permuteOutputs(tx)
}
}
def makeCommitTxTemplate(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): TxTemplate = {
val redeemScript = redeemSecretOrDelay(ourFinalKey, locktime2long_csv(theirDelay), theirFinalKey, revocationHash: BinaryData)
val htlcs = commitmentSpec.htlcs.filter(_.amountMsat >= 546000)
val htlcs = commitmentSpec.htlcs.filter(_.amountMsat >= 546000).toSeq
val fee_msat = computeFee(commitmentSpec.feeRate, htlcs.size) * 1000
val (amount_us_msat: Long, amount_them_msat: Long) = (commitmentSpec.amount_us_msat, commitmentSpec.amount_them_msat) match {
case (us, them) if us >= fee_msat / 2 && them >= fee_msat / 2 => (us - fee_msat / 2, them - fee_msat / 2)
@ -176,26 +212,27 @@ object Scripts {
case (us, them) if them < fee_msat / 2 => (Math.max(us - fee_msat + them, 0L), 0L)
}
val outputs = Seq(
// TODO : is that the correct way to handle sub-satoshi balances ?
TxOut(amount = Satoshi(amount_us_msat / 1000), publicKeyScript = pay2wsh(redeemScript)),
TxOut(amount = Satoshi(amount_them_msat / 1000), publicKeyScript = pay2wpkh(theirFinalKey))
).filterNot(_.amount.toLong < 546) // do not add dust
// our output is a pay2wsh output than can be claimed by them if they know the preimage, or by us after a delay
// when * they * publish a revoked commit tx, we use the preimage that they sent us to claim it
val ourOutput = if (amount_us_msat >= 546000) Some(P2WSH(Satoshi(amount_us_msat / 1000), redeemScript)) else None
val tx = Transaction(
version = 2,
txIn = inputs,
txOut = outputs,
lockTime = 0)
// their output is a simple pay2pkh output that sends money to their final key and can only be claimed by them
// when * they * publish a revoked commit tx we don't have anything special to do about it
val theirOutput = if (amount_them_msat >= 546000) Some(P2WPKH(Satoshi(amount_them_msat / 1000), theirFinalKey)) else None
val sendOuts = htlcs.filter(_.direction == OUT).map(htlc =>
TxOut(Satoshi(htlc.amountMsat / 1000), pay2wsh(scriptPubKeyHtlcSend(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash)))
)
val receiveOuts = htlcs.filter(_.direction == IN).map(htlc =>
TxOut(Satoshi(htlc.amountMsat / 1000), pay2wsh(scriptPubKeyHtlcReceive(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash)))
)
val tx1 = tx.copy(txOut = tx.txOut ++ sendOuts ++ receiveOuts)
permuteOutputs(tx1)
val sendOuts: Seq[OutputTemplate] = htlcs.filter(_.direction == OUT).map(htlc => {
P2WSH(Satoshi(htlc.amountMsat / 1000), scriptPubKeyHtlcSend(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash))
})
val receiveOuts: Seq[OutputTemplate] = htlcs.filter(_.direction == IN).map(htlc => {
P2WSH(Satoshi(htlc.amountMsat / 1000), scriptPubKeyHtlcReceive(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.expiry), locktime2long_csv(theirDelay), htlc.rHash, revocationHash))
})
TxTemplate(inputs, ourOutput, theirOutput, sendOuts, receiveOuts)
}
def makeCommitTx(inputs: Seq[TxIn], ourFinalKey: BinaryData, theirFinalKey: BinaryData, theirDelay: locktime, revocationHash: BinaryData, commitmentSpec: CommitmentSpec): Transaction = {
val txTemplate = makeCommitTxTemplate(inputs, ourFinalKey, theirFinalKey, theirDelay, revocationHash, commitmentSpec)
val tx = txTemplate.makeTx
tx
}
/**

View File

@ -1,6 +1,6 @@
package fr.acinq.eclair
import fr.acinq.bitcoin.{Base58, Base58Check, Crypto, Hash, OutPoint, Satoshi, TxIn, TxOut}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Crypto, Hash, OutPoint, Satoshi, TxIn, TxOut}
import fr.acinq.eclair.channel.{TheirChanges, _}
import fr.acinq.eclair.crypto.ShaChain
import lightning.locktime
@ -33,9 +33,9 @@ object TestConstants {
val commitments = Commitments(
Alice.channelParams,
TheirChannelParams(Bob.channelParams),
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Bob.revocationHash(0)),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(Bob.revocationHash(1)), anchorOutput, ShaChain.init)
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, BinaryData(""), Bob.revocationHash(0)),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
Right(Bob.revocationHash(1)), anchorOutput, ShaChain.init, new BasicTxDb)
}
@ -56,9 +56,9 @@ object TestConstants {
val commitments = Commitments(
Bob.channelParams,
TheirChannelParams(Alice.channelParams),
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, Alice.revocationHash(0)),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil),
Right(Alice.revocationHash(1)), anchorOutput, ShaChain.init)
OurCommit(0, ourSpec, ourTx), TheirCommit(0, theirSpec, BinaryData(""), Alice.revocationHash(0)),
OurChanges(Nil, Nil, Nil), TheirChanges(Nil, Nil), 0L,
Right(Alice.revocationHash(1)), anchorOutput, ShaChain.init, new BasicTxDb)
}
}

View File

@ -20,7 +20,7 @@ class StealRevokedCommitmentSpec extends FunSuite {
}
def addHtlc(sender: Commitments, receiver: Commitments, htlc: update_add_htlc): (Commitments, Commitments) = {
(Commitments.addOurProposal(sender, htlc), Commitments.addTheirProposal(receiver, htlc))
(Commitments.sendAdd(sender, CMD_ADD_HTLC(id = Some(htlc.id), amountMsat = htlc.amountMsat, rHash = htlc.rHash, expiry = htlc.expiry))._1, Commitments.receiveAdd(receiver, htlc))
}
def fulfillHtlc(sender: Commitments, receiver: Commitments, id: Long, paymentPreimage: BinaryData): (Commitments, Commitments) = {
@ -44,13 +44,22 @@ class StealRevokedCommitmentSpec extends FunSuite {
val (bob4, alice4) = fulfillHtlc(bob3, alice3, 1, R)
val (bob5, alice5) = signAndReceiveRevocation(bob4, alice4)
val theirTxTemplate = Commitments.makeTheirTxTemplate(bob3)
val theirTx = theirTxTemplate.makeTx
assert(theirTx.txIn == alice3.ourCommit.publishableTx.txIn && theirTx.txOut == alice3.ourCommit.publishableTx.txOut)
val preimage = bob5.theirPreimages.getHash(0xFFFFFFFFFFFFFFFFL - bob3.theirCommit.index).get
val punishTx = Helpers.claimRevokedCommitTx(theirTxTemplate, preimage, bob3.ourParams.finalPrivKey)
Transaction.correctlySpends(punishTx, Seq(alice3.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// now what if Alice published a revoked commit tx ?
Seq(alice1, alice2, alice3, alice4).map(alice => {
val stealTx = Helpers.claimTheirRevokedCommit(alice.ourCommit.publishableTx, bob5)
val stealTx = bob5.txDb.get(alice.ourCommit.publishableTx.txid)
Transaction.correctlySpends(stealTx.get, Seq(alice.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
})
// but we cannot steal Alice's current commit tx
assert(Helpers.claimTheirRevokedCommit(alice5.ourCommit.publishableTx, bob5) == None)
assert(bob5.txDb.get(alice5.ourCommit.publishableTx.txid) == None)
}
}

View File

@ -19,7 +19,7 @@ import scala.concurrent.duration._
@RunWith(classOf[JUnitRunner])
class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
override def withFixture(test: OneArgTest) = {
val alice2bob = TestProbe()
@ -39,14 +39,14 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
bob2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[WatchSpent]
awaitCond(bob.stateName == OPEN_WAITING_THEIRANCHOR)
test((bob, alice2bob, bob2alice, bob2blockchain))
test((alice, bob, alice2bob, bob2alice, bob2blockchain))
}
override def afterAll {
TestKit.shutdownActorSystem(system)
}
test("recv open_complete") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv open_complete") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
val msg = alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
@ -55,7 +55,7 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
}
}
test("recv BITCOIN_ANCHOR_DEPTHOK") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv BITCOIN_ANCHOR_DEPTHOK") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! BITCOIN_ANCHOR_DEPTHOK
awaitCond(bob.stateName == OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR)
@ -64,7 +64,7 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
}
}
test("recv BITCOIN_ANCHOR_TIMEOUT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv BITCOIN_ANCHOR_TIMEOUT") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! BITCOIN_ANCHOR_TIMEOUT
bob2alice.expectMsgType[error]
@ -72,23 +72,25 @@ class OpenWaitingTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with
}
}
test("recv BITCOIN_ANCHOR_SPENT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv BITCOIN_ANCHOR_SPENT") { case (alice, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
// this is the fully signed tx that alice could decide to publish
val tx = alice.stateData.asInstanceOf[DATA_OPEN_WAITING].commitments.ourCommit.publishableTx
// we have nothing at stake so we don't do anything with the tx
bob ! (BITCOIN_ANCHOR_SPENT, null)
bob ! (BITCOIN_ANCHOR_SPENT, tx)
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSED)
}
}
test("recv error") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv error") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! error(Some("oops"))
awaitCond(bob.stateName == CLOSED)
}
}
test("recv CMD_CLOSE") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv CMD_CLOSE") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! CMD_CLOSE(None)
awaitCond(bob.stateName == CLOSED)

View File

@ -19,7 +19,7 @@ import scala.concurrent.duration._
@RunWith(classOf[JUnitRunner])
class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
type FixtureParam = Tuple4[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe]
override def withFixture(test: OneArgTest) = {
val alice2bob = TestProbe()
@ -43,14 +43,14 @@ class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test"
bob2alice.expectMsgType[open_complete]
bob2alice.forward(alice)
awaitCond(bob.stateName == OPEN_WAIT_FOR_COMPLETE_THEIRANCHOR)
test((bob, alice2bob, bob2alice, bob2blockchain))
test((alice, bob, alice2bob, bob2alice, bob2blockchain))
}
override def afterAll {
TestKit.shutdownActorSystem(system)
}
test("recv open_complete") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv open_complete") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
@ -58,23 +58,25 @@ class OpenWaitForCompleteTheirAnchorStateSpec extends TestKit(ActorSystem("test"
}
}
test("recv BITCOIN_ANCHOR_SPENT") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv BITCOIN_ANCHOR_SPENT") { case (alice, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
// this is the fully signed tx that alice could decide to publish
val tx = alice.stateData.asInstanceOf[DATA_OPEN_WAITING].commitments.ourCommit.publishableTx
// we have nothing at stake so we don't do anything with the tx
bob ! (BITCOIN_ANCHOR_SPENT, null)
bob ! (BITCOIN_ANCHOR_SPENT, tx)
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSED)
}
}
test("recv CMD_CLOSE") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv CMD_CLOSE") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! CMD_CLOSE(None)
awaitCond(bob.stateName == CLOSED)
}
}
test("recv error") { case (bob, alice2bob, bob2alice, bob2blockchain) =>
test("recv error") { case (_, bob, alice2bob, bob2alice, bob2blockchain) =>
within(30 seconds) {
bob ! error(Some("oops"))
awaitCond(bob.stateName == CLOSED)

View File

@ -3,12 +3,12 @@ package fr.acinq.eclair.channel.simulator.states.e
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import com.google.protobuf.ByteString
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.{Crypto, Satoshi, Script, ScriptFlags, Transaction, TxOut}
import fr.acinq.eclair._
import fr.acinq.eclair.TestBitcoinClient
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, _}
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, Data, State, _}
import lightning._
import lightning.locktime.Locktime.Blocks
import org.junit.runner.RunWith
@ -16,6 +16,7 @@ import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, fixture}
import scala.concurrent.duration._
import scala.util.Random
/**
* Created by PM on 05/07/2016.
@ -69,13 +70,38 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
TestKit.shutdownActorSystem(system)
}
def addHtlc(amountMsat: Int, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (rval, update_add_htlc) = {
val rand = new Random()
val R = rval(rand.nextInt(), rand.nextInt(), rand.nextInt(), rand.nextInt())
val H: sha256_hash = Crypto.sha256(R)
val sender = TestProbe()
sender.send(s, CMD_ADD_HTLC(amountMsat, H, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = s2r.expectMsgType[update_add_htlc]
s2r.forward(r)
awaitCond(r.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed.contains(htlc))
(R, htlc)
}
def sign(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe) = {
val sender = TestProbe()
val rCommitIndex = r.stateData.asInstanceOf[HasCommitments].commitments.ourCommit.index
sender.send(s, CMD_SIGN)
sender.expectMsg("ok")
s2r.expectMsgType[update_commit]
s2r.forward(r)
r2s.expectMsgType[update_revocation]
r2s.forward(s)
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.ourCommit.index == rCommitIndex + 1)
}
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val h = sha256_hash(1, 2, 3, 4)
alice ! CMD_ADD_HTLC(500000, h, locktime(Blocks(3)))
val htlc = alice2bob.expectMsgType[update_add_htlc]
assert(htlc.id == 1 && htlc.rHash == h)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].htlcIdx == 1 && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCurrentHtlcId == 1 && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
}
}
@ -161,11 +187,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_SIGN") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
alice2bob.expectMsgType[update_add_htlc]
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
@ -185,11 +207,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
alice2bob.expectMsgType[update_add_htlc]
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.theirNextCommitInfo.isRight)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -203,14 +221,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_commit") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_SIGN)
@ -225,6 +237,33 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
test("recv update_commit (two htlcs with same r)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(5000000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc1 = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
sender.send(alice, CMD_ADD_HTLC(5000000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc2 = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc1 :: htlc2 :: Nil)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sign(alice, bob, alice2bob, bob2alice)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.htlcs.exists(h => h.id == htlc1.id && h.direction == IN))
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.htlcs.exists(h => h.id == htlc2.id && h.direction == IN))
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.spec.amount_us_msat == initialState.commitments.ourCommit.spec.amount_us_msat)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx.txOut.count(_.amount == Satoshi(5000)) == 2)
}
}
ignore("recv update_commit (no changes)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
@ -241,14 +280,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_commit (invalid signature)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
// actual test begins
@ -263,14 +295,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_revocation") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CMD_SIGN)
@ -290,14 +315,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
val r = sha256_hash(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -330,22 +348,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_FULFILL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
@ -371,22 +375,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_FULFILL_HTLC (invalid preimage)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
@ -399,22 +389,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_fulfill_htlc") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r))
sender.expectMsg("ok")
@ -443,22 +419,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
sender.send(alice, update_fulfill_htlc(42, rval(0, 0, 0, 0)))
@ -472,22 +434,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_FAIL_HTLC") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
@ -513,22 +461,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_fail_htlc") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FAIL_HTLC(htlc.id, "some reason"))
sender.expectMsg("ok")
@ -587,14 +521,10 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
ignore("recv close_clearing (with unacked received htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
ignore("recv close_clearing (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
// actual test begins
sender.send(alice, close_clearing(ByteString.EMPTY))
alice2bob.expectMsgType[close_clearing]
@ -602,14 +532,10 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
ignore("recv close_clearing (with unacked sent htlcs)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
ignore("recv close_clearing (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
// actual test begins
sender.send(alice, close_clearing(ByteString.EMPTY))
alice2bob.expectMsgType[close_clearing]
@ -620,21 +546,8 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv close_clearing (with signed htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == htlc :: Nil)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.proposed == Nil && alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourChanges.acked == htlc :: Nil)
val (r, htlc) = addHtlc(500000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
sender.send(alice, close_clearing(ByteString.EMPTY))
@ -643,12 +556,54 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
ignore("recv BITCOIN_ANCHOR_SPENT") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv BITCOIN_ANCHOR_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice ! (BITCOIN_ANCHOR_SPENT, null)
val sender = TestProbe()
// alice sends 300 000 sat and bob fulfills
// we reuse the same r (it doesn't matter here)
val (r, htlc) = addHtlc(300000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FULFILL_HTLC(1, r))
sender.expectMsg("ok")
val fulfill = bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
sign(bob, alice, bob2alice, alice2bob)
// at this point we have :
// alice = 700 000
// bob = 300 000
def send(): Transaction = {
// alice sends 1 000 sat
// we reuse the same r (it doesn't matter here)
val (r, htlc) = addHtlc(1000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
}
val txs = for (i <- 0 until 10) yield send()
// bob now has 10 spendable tx, 9 of them being revoked
// let's say that bob published this tx
val revokedTx = txs(3)
// channel state for this revoked tx is as follows:
// alice = 696 000
// bob = 300 000
// a->b = 4 000
alice ! (BITCOIN_ANCHOR_SPENT, revokedTx)
alice2bob.expectMsgType[error]
// TODO
val punishTx = alice2blockchain.expectMsgType[Publish].tx
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateName == CLOSING)
Transaction.correctlySpends(punishTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// two main outputs + 4 htlc
assert(revokedTx.txOut.size == 6)
// the punishment tx consumes all output but ours (which already goes to our final key)
assert(punishTx.txIn.size == 5)
// TODO : when changefee is implemented we should set fee = 0 and check against 304 000
assert(punishTx.txOut == Seq(TxOut(Satoshi(301670), Script.write(Scripts.pay2wpkh(Alice.finalPubKey)))))
}
}

View File

@ -45,7 +45,7 @@
<maven.compiler.target>1.7</maven.compiler.target>
<scala.version>2.11.8</scala.version>
<scala.version.short>2.11</scala.version.short>
<akka.version>2.4.6</akka.version>
<akka.version>2.4.8</akka.version>
<bitcoinlib.version>0.9.6-RC2</bitcoinlib.version>
<acinqtools.version>1.2</acinqtools.version>
</properties>