1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-27 02:37:06 +01:00

merged from wip-uniclose

This commit is contained in:
pm47 2016-08-24 15:46:47 +02:00
commit b1589a9a5f
22 changed files with 1045 additions and 349 deletions

View file

@ -11,42 +11,34 @@ import org.json4s.JsonAST.{JNull, JString}
/**
* Created by PM on 28/01/2016.
*/
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => (
{
case JString(hex) if (false) => // NOT IMPLEMENTED
???
},
{
case x: BinaryData => JString(x.toString())
}
class BinaryDataSerializer extends CustomSerializer[BinaryData](format => ( {
case JString(hex) if (false) => // NOT IMPLEMENTED
???
}, {
case x: BinaryData => JString(x.toString())
}
))
class StateSerializer extends CustomSerializer[State](format => (
{
case JString(x) if (false) => // NOT IMPLEMENTED
???
},
{
case x: State => JString(x.toString())
}
class StateSerializer extends CustomSerializer[State](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: State => JString(x.toString())
}
))
class Sha256Serializer extends CustomSerializer[sha256_hash](format => (
{
case JString(x) if (false) => // NOT IMPLEMENTED
???
},
{
case x: sha256_hash => JString(sha2562bin(x).toString())
}
class Sha256Serializer extends CustomSerializer[sha256_hash](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: sha256_hash => JString(sha2562bin(x).toString())
}
))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => (
{
case JString(x) if (false) => // NOT IMPLEMENTED
???
},
{
case x: ShaChain => JNull
}
))
class ShaChainSerializer extends CustomSerializer[ShaChain](format => ( {
case JString(x) if (false) => // NOT IMPLEMENTED
???
}, {
case x: ShaChain => JNull
}
))

View file

@ -4,10 +4,11 @@ package fr.acinq.eclair.blockchain
import akka.actor.{Actor, ActorLogging, Cancellable, Terminated}
import akka.pattern.pipe
import fr.acinq.bitcoin._
import fr.acinq.eclair.channel.BITCOIN_ANCHOR_SPENT
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_SPENT, Scripts}
import org.bouncycastle.util.encoders.Hex
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.collection.SortedMap
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
/**
@ -17,11 +18,20 @@ import scala.concurrent.duration._
*/
class PollingWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
context.become(watching(Map()))
override def receive: Receive = ???
def watching(watches: Map[Watch, Cancellable]): Receive = {
context.become(watching(Map(), SortedMap(), 0))
context.system.scheduler.schedule(150 milliseconds, 10 seconds)(client.getBlockCount.map(count => ('currentBlockCount, count)).pipeTo(self))
def publish(tx: Transaction): Unit = {
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))}")
}
}
def watching(watches: Map[Watch, Cancellable], block2tx: SortedMap[Long, Seq[Transaction]], currentBlockCount: Long): Receive = {
case w: WatchLost => log.warning(s"ignoring $w (not implemented)")
@ -33,7 +43,7 @@ class PollingWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContex
client.getTxConfirmations(txId.toString).map(_ match {
case Some(confirmations) if confirmations >= minDepth =>
channel ! event
self !('remove, w)
self ! ('remove, w)
case _ => {}
})
case w@WatchSpent(channel, txId, outputIndex, minDepth, event) =>
@ -43,22 +53,31 @@ class PollingWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContex
} yield {
if (conf.isDefined && !unspent) {
// NOTE : isSpent=!isUnspent only works if the parent transaction actually exists (which we assume to be true)
client.findSpendingTransaction(txId.toString(), outputIndex).map(tx => channel !(BITCOIN_ANCHOR_SPENT, tx))
self !('remove, w)
client.findSpendingTransaction(txId.toString(), outputIndex).map(tx => channel ! (BITCOIN_ANCHOR_SPENT, tx))
self ! ('remove, w)
} else {}
}
})
context.become(watching(watches + (w -> cancellable)))
context.become(watching(watches + (w -> cancellable), block2tx, currentBlockCount))
case ('remove, w: Watch) if watches.contains(w) =>
watches(w).cancel()
context.become(watching(watches - w))
context.become(watching(watches - w, block2tx, currentBlockCount))
case Publish(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))}")
}
case ('currentBlockCount, count: Long) => {
val topublish = block2tx.filterKeys(_ <= count)
topublish.values.flatten.map(publish)
context.become(watching(watches, block2tx -- topublish.keys, count))
}
case Publish(tx) => publish(tx)
case PublishAsap(tx) =>
val cltvTimeout = Scripts.cltvTimeout(tx)
val csvTimeout = currentBlockCount + Scripts.csvTimeout(tx)
val timeout = Math.max(cltvTimeout, csvTimeout)
val block2tx1 = block2tx.updated(timeout, tx +: block2tx.getOrElse(timeout, Seq.empty[Transaction]))
context.become(watching(watches, block2tx1, currentBlockCount))
case MakeAnchor(ourCommitPub, theirCommitPub, amount) =>
client.makeAnchorTx(ourCommitPub, theirCommitPub, amount).pipeTo(sender)
@ -66,6 +85,10 @@ class PollingWatcher(client: ExtendedBitcoinClient)(implicit ec: ExecutionContex
case Terminated(subject) =>
val deadWatches = watches.keys.filter(_.channel == subject)
deadWatches.map(w => watches(w).cancel())
context.become(watching(watches -- deadWatches))
context.become(watching(watches -- deadWatches, block2tx, currentBlockCount))
}
}
object PollingWatcher {
}

View file

@ -13,17 +13,13 @@ import fr.acinq.eclair.channel.BlockchainEvent
trait Watch {
def channel: ActorRef
}
final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, minDepth: Int, event: BlockchainEvent) extends Watch
final case class WatchSpent(channel: ActorRef, txId: BinaryData, outputIndex: Int, minDepth: Int, event: BlockchainEvent) extends Watch
// notify me if confirmation number gets below minDepth
final case class WatchLost(channel: ActorRef, txId: BinaryData, minDepth: Int, event: BlockchainEvent) extends Watch
// notify me if confirmation number gets below minDepth
final case class Publish(tx: Transaction)
final case class PublishAsap(tx: Transaction)
final case class MakeAnchor(ourCommitPub: BinaryData, theirCommitPub: BinaryData, amount: Satoshi)
// @formatter:on

View file

@ -281,7 +281,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
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)
case Event(e@error(problem), d: DATA_NORMAL) => handleTheirError(e, d)
})
@ -297,6 +297,11 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
*/
when(NORMAL) {
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) if d.ourClearing.isDefined =>
handleCommandError(sender, new RuntimeException("cannot send new htlcs, closing in progress"))
stay
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, origin, id_opt, do_commit), d@DATA_NORMAL(commitments, _, downstreams)) =>
Try(Commitments.sendAdd(commitments, c)) match {
case Success((commitments1, add)) =>
@ -310,7 +315,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Success(commitments1) =>
commitments1.ourParams.autoSignInterval.map(interval => context.system.scheduler.scheduleOnce(interval, self, CMD_SIGN))
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(c@CMD_FULFILL_HTLC(id, r, do_commit), d: DATA_NORMAL) =>
@ -327,7 +332,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
propagateDownstream(Right(fulfill), downstreams)
commitments1.ourParams.autoSignInterval.map(interval => context.system.scheduler.scheduleOnce(interval, self, CMD_SIGN))
stay using d.copy(commitments = commitments1, downstreams = downstreams - id)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(c@CMD_FAIL_HTLC(id, reason, do_commit), d: DATA_NORMAL) =>
@ -344,7 +349,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
propagateDownstream(Left(fail), downstreams)
commitments1.ourParams.autoSignInterval.map(interval => context.system.scheduler.scheduleOnce(interval, self, CMD_SIGN))
stay using d.copy(commitments = commitments1, downstreams = downstreams - id)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(CMD_SIGN, d: DATA_NORMAL) if d.commitments.theirNextCommitInfo.isLeft =>
@ -375,7 +380,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
.foreach(htlc => propagateUpstream(htlc.add, d.commitments.anchorId))
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d: DATA_NORMAL) =>
@ -384,7 +389,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(CMD_CLOSE(scriptPubKeyOpt), d: DATA_NORMAL) =>
@ -421,7 +426,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_NORMAL) => handleTheirSpentOther(tx, d)
case Event(e@error(problem), d: DATA_NORMAL) => handleError(e, d)
case Event(e@error(problem), d: DATA_NORMAL) => handleTheirError(e, d)
}
@ -448,7 +453,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Success(commitments1) =>
propagateDownstream(Right(fulfill), d.downstreams)
stay using d.copy(commitments = commitments1, downstreams = d.downstreams - id)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(c@CMD_FAIL_HTLC(id, reason, do_commit), d: DATA_CLEARING) =>
@ -462,7 +467,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Success(commitments1) =>
propagateDownstream(Left(fail), d.downstreams)
stay using d.copy(commitments = commitments1, downstreams = d.downstreams - id)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(CMD_SIGN, d: DATA_CLEARING) if d.commitments.theirNextCommitInfo.isLeft =>
@ -494,7 +499,7 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
case Success((commitments1, revocation)) =>
them ! revocation
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event(msg@update_revocation(revocationPreimage, nextRevocationHash), d@DATA_CLEARING(commitments, ourClearing, theirClearing, _)) =>
@ -507,14 +512,14 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, ourClearing, theirClearing, ourCloseSig)
case Success(commitments1) =>
stay using d.copy(commitments = commitments1)
case Failure(cause) => handleUnicloseError(cause, d)
case Failure(cause) => handleOurError(cause, d)
}
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) if tx.txid == d.commitments.theirCommit.txid => handleTheirSpentCurrent(tx, d)
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLEARING) => handleTheirSpentOther(tx, d)
case Event(e@error(problem), d: DATA_CLEARING) => handleError(e, d)
case Event(e@error(problem), d: DATA_CLEARING) => handleTheirError(e, d)
}
when(NEGOTIATING) {
@ -551,26 +556,44 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
throw new RuntimeException("cannot verify their close signature", cause)
}
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) if tx.txid == d.commitments.theirCommit.txid => 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) => handleTheirError(e, d)
}
when(CLOSING) {
case Event(close_signature(theirCloseFee, theirSig), d: DATA_CLOSING) if d.ourSignature.map(_.closeFee) == Some(theirCloseFee) =>
// expected in case of a mutual close
stay()
case Event(BITCOIN_CLOSE_DONE, _) => goto(CLOSED)
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.ourCommit.publishableTx.txid =>
// we just initiated a uniclose moments ago and are now receiving the blockchain notification, there is nothing to do
stay()
case Event(e@error(problem), _) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
// TODO not implemented
goto(CLOSED)
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLOSING) if Some(tx.txid) == d.mutualClosePublished.map(_.txid) =>
// we just published a mutual close tx, we are notified but it's alright
stay()
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLOSING) if tx.txid == d.commitments.theirCommit.txid =>
// counterparty may attempt to spend its last commit tx at any time
handleTheirSpentCurrent(tx, d)
case Event((BITCOIN_ANCHOR_SPENT, tx: Transaction), d: DATA_CLOSING) =>
// counterparty may attempt to spend a revoked commit tx at any time
handleTheirSpentOther(tx, d)
case Event(BITCOIN_CLOSE_DONE, d: DATA_CLOSING) if d.mutualClosePublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_OURS_DONE, d: DATA_CLOSING) if d.ourCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_SPEND_THEIRS_DONE, d: DATA_CLOSING) if d.theirCommitPublished.isDefined => goto(CLOSED)
case Event(BITCOIN_STEAL_DONE, d: DATA_CLOSING) if d.revokedPublished.size > 0 => goto(CLOSED)
case Event(e@error(problem), d: DATA_CLOSING) => stay // nothing to do, there is already a spending tx published
}
when(CLOSED, stateTimeout = 30 seconds) {
@ -608,10 +631,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
}, stateName, stateData)
stay
// because channels send CMD to each others when relaying payments
case Event("ok", _) => stay
// TODO : them ! error(Some("Unexpected message")) ?
}
onTransition {
@ -691,18 +712,53 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
stay
}
def handleUnicloseError(cause: Throwable, d: HasCommitments) = {
def handleOurError(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))
spendOurCurrent(d)
}
def handleTheirError(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
spendOurCurrent(d)
}
def spendOurCurrent(d: HasCommitments) = {
val tx = d.commitments.ourCommit.publishableTx
blockchain ! Publish(tx)
blockchain ! WatchConfirmed(self, tx.txid, d.commitments.ourParams.minDepth, BITCOIN_SPEND_OURS_DONE)
val txs1 = Helpers.claimReceivedHtlcs(tx, Commitments.makeOurTxTemplate(d.commitments), d.commitments)
val txs2 = Helpers.claimSentHtlcs(tx, Commitments.makeOurTxTemplate(d.commitments), d.commitments)
val txs = txs1 ++ txs2
txs.map(tx => blockchain ! PublishAsap(tx))
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(ourCommitPublished = Some(tx))
case _ => DATA_CLOSING(d.commitments, ourCommitPublished = Some(tx))
}
goto(CLOSING) using nextData
}
def handleTheirSpentCurrent(tx: Transaction, d: HasCommitments) = {
log.warning(s"they published their current commit in txid=${tx.txid}")
// TODO
???
assert(tx.txid == d.commitments.theirCommit.txid)
blockchain ! WatchConfirmed(self, tx.txid, d.commitments.ourParams.minDepth, BITCOIN_SPEND_THEIRS_DONE)
val txs1 = Helpers.claimReceivedHtlcs(tx, Commitments.makeTheirTxTemplate(d.commitments), d.commitments)
val txs2 = Helpers.claimSentHtlcs(tx, Commitments.makeTheirTxTemplate(d.commitments), d.commitments)
val txs = txs1 ++ txs2
txs.map(tx => blockchain ! PublishAsap(tx))
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(theirCommitPublished = Some(tx))
case _ => DATA_CLOSING(d.commitments, theirCommitPublished = Some(tx))
}
goto(CLOSING) using nextData
}
def handleTheirSpentOther(tx: Transaction, d: HasCommitments) = {
@ -712,8 +768,12 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
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))
blockchain ! WatchConfirmed(self, spendingTx.txid, d.commitments.ourParams.minDepth, BITCOIN_STEAL_DONE)
val nextData = d match {
case closing: DATA_CLOSING => closing.copy(revokedPublished = closing.revokedPublished :+ tx)
case _ => DATA_CLOSING(d.commitments, revokedPublished = Seq(tx))
}
goto(CLOSING) using nextData
case None =>
// the published tx was neither their current commitment nor a revoked one
log.error(s"couldn't identify txid=${tx.txid}!")
@ -721,13 +781,6 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
}
}
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 !!")
@ -745,13 +798,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, paymentHandler: Acto
s(event)
} catch {
case t: Throwable => event.stateData match {
case d: HasCommitments =>
log.error(t, "error, closing")
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 _ =>
goto(CLOSED)
case d: HasCommitments => handleOurError(t, d)
case _ => goto(CLOSED)
}
}
}

View file

@ -76,6 +76,8 @@ case object INPUT_NO_MORE_HTLCS
// when requesting a mutual close, we wait for as much as this timeout, then unilateral close
case object INPUT_CLOSE_COMPLETE_TIMEOUT
sealed trait BlockchainEvent
case object BITCOIN_ANCHOR_DEPTHOK extends BlockchainEvent
@ -96,6 +98,8 @@ case object BITCOIN_STEAL_DONE extends BlockchainEvent
case object BITCOIN_CLOSE_DONE extends BlockchainEvent
case class TransactionConfirmed(tx: Transaction) extends BlockchainEvent
/*
.d8888b. .d88888b. 888b d888 888b d888 d8888 888b 888 8888888b. .d8888b.
d88P Y88b d88P" "Y88b 8888b d8888 8888b d8888 d88888 8888b 888 888 "Y88b d88P Y88b
@ -170,7 +174,7 @@ final case class CommitmentSpec(htlcs: Set[Htlc], feeRate: Long, initial_amount_
final case class ClosingData(ourScriptPubKey: BinaryData, theirScriptPubKey: Option[BinaryData])
trait HasCommitments {
trait HasCommitments extends Data {
def commitments: Commitments
}

View file

@ -30,7 +30,9 @@ class BasicTxDb extends TxDb {
object TypeDefs {
type Change = GeneratedMessage
}
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change])
case class OurChanges(proposed: List[Change], signed: List[Change], acked: List[Change]) {
def all: List[Change] = proposed ++ signed ++ acked
}
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)
@ -41,7 +43,7 @@ case class TheirCommit(index: Long, spec: CommitmentSpec, txid: BinaryData, thei
/**
* about theirNextCommitInfo:
* we either:
* - have built and sign their next commit tx with their next revocation hash which can now be discarded
* - have built and signed 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
@ -81,8 +83,8 @@ object Commitments {
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.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(-_.add.amountMsat).sum
// a node cannot spend pending incoming htlcs
val available = reduced.amount_them_msat
if (cmd.amountMsat > available) {
throw new RuntimeException(s"insufficient funds (available=$available msat)")
} else {
@ -96,8 +98,8 @@ object Commitments {
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.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(-_.add.amountMsat).sum
// a node cannot spend pending incoming htlcs
val available = reduced.amount_them_msat
if (add.amountMsat > available) {
throw new RuntimeException("Insufficient funds")
} else {
@ -230,6 +232,11 @@ object Commitments {
}
}
def makeOurTxTemplate(commitments: Commitments): TxTemplate = {
Helpers.makeOurTxTemplate(commitments.ourParams, commitments.theirParams, commitments.ourCommit.publishableTx.txIn,
Helpers.revocationHash(commitments.ourParams.shaSeed, commitments.ourCommit.index), commitments.ourCommit.spec)
}
def makeTheirTxTemplate(commitments: Commitments): TxTemplate = {
commitments.theirNextCommitInfo match {
case Left(theirNextCommit) =>

View file

@ -1,12 +1,13 @@
package fr.acinq.eclair.channel
import Scripts._
import fr.acinq.bitcoin._
import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair._
import fr.acinq.eclair.channel.TypeDefs.Change
import fr.acinq.eclair.crypto.ShaChain
import lightning._
import scala.annotation.tailrec
import scala.util.Try
/**
@ -67,6 +68,9 @@ object Helpers {
spec4
}
def makeOurTxTemplate(ourParams: OurChannelParams, theirParams: TheirChannelParams, inputs: Seq[TxIn], ourRevocationHash: sha256_hash, spec: CommitmentSpec): TxTemplate =
makeCommitTxTemplate(inputs, ourParams.finalPubKey, theirParams.finalPubKey, ourParams.delay, ourRevocationHash, spec)
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)
@ -155,84 +159,21 @@ object Helpers {
def revocationHash(seed: BinaryData, index: Long): BinaryData = Crypto.sha256(revocationPreimage(seed, index))
/**
* Claim their revoked commit tx. If they published a revoked commit tx, we should be able to "steal" it with one
* of the revocation preimages that we received.
* Remainder: their commit tx sends:
* - our money to our final key
* - their money to (their final key + our delay) OR (our final key + secret)
* We don't have anything to do about our output (which should probably show up in our bitcoin wallet), can steal their
* money if we can find the preimage.
* We use a basic brute force algorithm: try all the preimages that we have until we find a match
* Claim a revoked commit tx using the matching revocation preimage, which allows us to claim all its inputs without a
* delay
*
* @param commitTx their revoked commit tx
* @param commitments our commitment data
* @return an optional transaction which "steals" their output
*/
def claimTheirRevokedCommit(commitTx: Transaction, commitments: Commitments): Option[Transaction] = {
// this is what their output script looks like
def theirOutputScript(preimage: BinaryData) = {
val revocationHash = Crypto.sha256(preimage)
redeemSecretOrDelay(commitments.theirParams.finalPubKey, locktime2long_csv(commitments.theirParams.delay), commitments.ourParams.finalPubKey, revocationHash)
}
// find an output that we can claim with one of our preimages
// the only that we're looking for is a pay-to-script (pay2wsh) so for each output that we try we need to generate
// all possible output scripts, hash them and see if they match
def findTheirOutputPreimage: Option[(Int, BinaryData)] = {
for (i <- 0 until commitTx.txOut.length) {
val actual = Script.parse(commitTx.txOut(i).publicKeyScript)
val preimage = commitments.theirPreimages.iterator.find(preimage => {
val expected = theirOutputScript(preimage)
val hashOfExpected = pay2wsh(expected)
hashOfExpected == actual
})
preimage.map(value => return Some(i, value))
}
None
}
findTheirOutputPreimage map {
case (index, preimage) =>
// TODO: substract network fee
val amount = commitTx.txOut(index).amount
val tx = Transaction(version = 2,
txIn = TxIn(OutPoint(commitTx, index), BinaryData.empty, TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(amount, pay2pkh(commitments.ourParams.finalPubKey)) :: Nil,
lockTime = 0xffffffffL)
val redeemScript: BinaryData = Script.write(theirOutputScript(preimage))
val sig: BinaryData = Transaction.signInput(tx, 0, redeemScript, SIGHASH_ALL, amount, 1, commitments.ourParams.finalPrivKey, randomize = false)
val witness = ScriptWitness(sig :: preimage :: redeemScript :: Nil)
val tx1 = tx.copy(witness = Seq(witness))
// check that we can actually spend the commit tx
Transaction.correctlySpends(tx1, commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
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
* @param theirTxTemplate revoked commit tx template
* @param revocationPreimage revocation preimage (which must match this specific commit tx)
* @param privateKey private key to send the claimed funds to (the returned tx will include a single P2WPKH output)
* @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
val outputsToClaim = (theirTxTemplate.ourOutput.toSeq ++ theirTxTemplate.htlcReceived ++ theirTxTemplate.htlcSent).filter(o => theirTx.txOut.indexOf(o.txOut) != -1)
val totalAmount = outputsToClaim.map(_.amount).sum // TODO: substract a small network fee
// create a tx that sends everything to our private key
val tx = Transaction(version = 2,
@ -243,14 +184,14 @@ object Helpers {
// create tx inputs that spend each output that we can spend
val inputs = outputsToClaim.map(outputTemplate => {
val index = findOutputIndex(outputTemplate.txOut).get
val index = theirTx.txOut.indexOf(outputTemplate.txOut)
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 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
@ -258,4 +199,80 @@ object Helpers {
tx1.copy(witness = witnesses)
}
/**
* claim an HTLC that we received using its payment preimage. This is used only when the other party publishes its
* current commit tx which contains pending HTLCs.
*
* @param tx commit tx published by the other party
* @param htlcTemplate HTLC template for an HTLC in the commit tx for which we have the preimage
* @param paymentPreimage HTLC preimage
* @param privateKey private key which matches the pubkey that the HTLC was sent to
* @return a signed transaction that spends the HTLC in their published commit tx.
* This tx is not spendable right away: it has both an absolute CLTV time-out and a relative CSV time-out
* before which it can be published
*/
def claimReceivedHtlc(tx: Transaction, htlcTemplate: HtlcTemplate, paymentPreimage: BinaryData, privateKey: BinaryData): Transaction = {
require(htlcTemplate.htlc.add.rHash == bin2sha256(Crypto.sha256(paymentPreimage)), "invalid payment preimage")
// find its index in their tx
val index = tx.txOut.indexOf(htlcTemplate.txOut)
val tx1 = Transaction(version = 2,
txIn = TxIn(OutPoint(tx, index), BinaryData.empty, sequence = Scripts.locktime2long_csv(htlcTemplate.delay)) :: Nil,
txOut = TxOut(htlcTemplate.amount, Scripts.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
lockTime = Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry))
val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey)
val witness = ScriptWitness(sig :: paymentPreimage :: htlcTemplate.redeemScript :: Nil)
val tx2 = tx1.copy(witness = Seq(witness))
tx2
}
/**
* claim all the HTLCs that we've received from their current commit tx
*
* @param txTemplate commit tx published by the other party
* @param commitments our commitment data, which include payment preimages
* @return a list of transactions (one per HTLC that we can claim)
*/
def claimReceivedHtlcs(tx: Transaction, txTemplate: TxTemplate, commitments: Commitments): Seq[Transaction] = {
val preImages = commitments.ourChanges.all.collect { case update_fulfill_htlc(id, r) => rval2bin(r) }
// TODO: FIXME !!!
//val htlcTemplates = txTemplate.htlcSent
val htlcTemplates = txTemplate.htlcReceived ++ txTemplate.htlcSent
@tailrec
def loop(htlcs: Seq[HtlcTemplate], acc: Seq[Transaction] = Seq.empty[Transaction]): Seq[Transaction] = {
htlcs.headOption match {
case Some(head) =>
preImages.find(preImage => head.htlc.add.rHash == bin2sha256(Crypto.sha256(preImage))) match {
case Some(preImage) => loop(htlcs.tail, claimReceivedHtlc(tx, head, preImage, commitments.ourParams.finalPrivKey) +: acc)
case None => loop(htlcs.tail, acc)
}
case None => acc
}
}
loop(htlcTemplates)
}
def claimSentHtlc(tx: Transaction, htlcTemplate: HtlcTemplate, privateKey: BinaryData): Transaction = {
val index = tx.txOut.indexOf(htlcTemplate.txOut)
val tx1 = Transaction(
version = 2,
txIn = TxIn(OutPoint(tx, index), Array.emptyByteArray, sequence = Scripts.locktime2long_csv(htlcTemplate.delay)) :: Nil,
txOut = TxOut(htlcTemplate.amount, Scripts.pay2pkh(Crypto.publicKeyFromPrivateKey(privateKey))) :: Nil,
lockTime = Scripts.locktime2long_cltv(htlcTemplate.htlc.add.expiry))
val sig = Transaction.signInput(tx1, 0, htlcTemplate.redeemScript, SIGHASH_ALL, htlcTemplate.amount, 1, privateKey)
val witness = ScriptWitness(sig :: Hash.Zeroes :: htlcTemplate.redeemScript :: Nil)
tx1.copy(witness = Seq(witness))
}
def claimSentHtlcs(tx: Transaction, txTemplate: TxTemplate, commitments: Commitments): Seq[Transaction] = {
// txTemplate could be our template (we published our commit tx) or their template (they published their commit tx)
val htlcs1 = txTemplate.htlcSent.filter(_.ourKey == commitments.ourParams.finalPubKey)
val htlcs2 = txTemplate.htlcReceived.filter(_.theirKey == commitments.ourParams.finalPubKey)
val htlcs = htlcs1 ++ htlcs2
htlcs.map(htlcTemplate => claimSentHtlc(tx, htlcTemplate, commitments.ourParams.finalPrivKey))
}
}

View file

@ -73,11 +73,15 @@ object Register {
def create_alias(node_id: BinaryData, anchor_id: BinaryData)(implicit context: ActorContext) =
context.actorOf(Props(new AliasActor(context.self)), name = s"$node_id-$anchor_id")
def actorPathToNodeId(nodeId: BinaryData)(implicit context: ActorContext): ActorPath =
context.system / "register" / "auth-handler-*" / "channel" / s"${nodeId}-*"
def actorPathToNodeId(system: ActorSystem, nodeId: BinaryData): ActorPath =
system / "register" / "auth-handler-*" / "channel" / s"${nodeId}-*"
def actorPathToChannelId(channelId: BinaryData)(implicit context: ActorContext): ActorPath =
context.system / "register" / "auth-handler-*" / "channel" / s"*-${channelId}"
def actorPathToNodeId(nodeId: BinaryData)(implicit context: ActorContext): ActorPath = actorPathToNodeId(context.system, nodeId)
def actorPathToChannelId(system: ActorSystem, channelId: BinaryData): ActorPath =
system / "register" / "auth-handler-*" / "channel" / s"*-${channelId}"
def actorPathToChannelId(channelId: BinaryData)(implicit context: ActorContext): ActorPath = actorPathToChannelId(context.system, channelId)
def actorPathToChannels()(implicit context: ActorContext): ActorPath =
context.system / "register" / "auth-handler-*" / "channel"

View file

@ -122,6 +122,8 @@ object Scripts {
}
def scriptPubKeyHtlcSend(ourkey: BinaryData, theirkey: BinaryData, abstimeout: Long, reltimeout: Long, rhash: BinaryData, commit_revoke: BinaryData): Seq[ScriptElt] = {
// values lesser than 16 should be encoded using OP_0..OP_16 instead of OP_PUSHDATA
assert(abstimeout > 16, s"abstimeout=$abstimeout must be greater than 16")
// @formatter:off
OP_SIZE :: OP_PUSHDATA(encodeNumber(32)) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_DUP ::
@ -137,6 +139,8 @@ object Scripts {
}
def scriptPubKeyHtlcReceive(ourkey: BinaryData, theirkey: BinaryData, abstimeout: Long, reltimeout: Long, rhash: BinaryData, commit_revoke: BinaryData): Seq[ScriptElt] = {
// values lesser than 16 should be encoded using OP_0..OP_16 instead of OP_PUSHDATA
assert(abstimeout > 16, s"abstimeout=$abstimeout must be greater than 16")
// @formatter:off
OP_SIZE :: OP_PUSHDATA(encodeNumber(32)) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_DUP ::
@ -175,8 +179,20 @@ object Scripts {
def redeemScript: BinaryData
}
case class HtlcTemplate(htlc: Htlc, ourKey: BinaryData, theirKey: BinaryData, delay: locktime, revocationHash: BinaryData) extends OutputTemplate {
override def amount = Satoshi(htlc.add.amountMsat / 1000)
override def redeemScript = htlc.direction match {
case IN => Script.write(Scripts.scriptPubKeyHtlcReceive(ourKey, theirKey, locktime2long_cltv(htlc.add.expiry), locktime2long_csv(delay), htlc.add.rHash, revocationHash))
case OUT => Script.write(Scripts.scriptPubKeyHtlcSend(ourKey, theirKey, locktime2long_cltv(htlc.add.expiry), locktime2long_csv(delay), htlc.add.rHash, revocationHash))
}
override def txOut = TxOut(amount, pay2wsh(redeemScript))
}
case class P2WSH(amount: Satoshi, script: BinaryData) extends OutputTemplate {
override def txOut: TxOut = TxOut(amount, pay2wsh(script))
override def redeemScript = script
}
@ -186,10 +202,11 @@ object Scripts {
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]) {
case class TxTemplate(inputs: Seq[TxIn], ourOutput: Option[OutputTemplate], theirOutput: Option[OutputTemplate], htlcSent: Seq[HtlcTemplate], htlcReceived: Seq[HtlcTemplate]) {
def makeTx: Transaction = {
val outputs = ourOutput.toSeq ++ theirOutput.toSeq ++ htlcSent ++ htlcReceived
val tx = Transaction(
@ -220,11 +237,11 @@ object Scripts {
// 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: Seq[OutputTemplate] = htlcs.filter(_.direction == OUT).map(htlc => {
P2WSH(Satoshi(htlc.add.amountMsat / 1000), scriptPubKeyHtlcSend(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.add.expiry), locktime2long_csv(theirDelay), htlc.add.rHash, revocationHash))
val sendOuts: Seq[HtlcTemplate] = htlcs.filter(_.direction == OUT).map(htlc => {
HtlcTemplate(htlc, ourFinalKey, theirFinalKey, theirDelay, revocationHash)
})
val receiveOuts: Seq[OutputTemplate] = htlcs.filter(_.direction == IN).map(htlc => {
P2WSH(Satoshi(htlc.add.amountMsat / 1000), scriptPubKeyHtlcReceive(ourFinalKey, theirFinalKey, locktime2long_cltv(htlc.add.expiry), locktime2long_csv(theirDelay), htlc.add.rHash, revocationHash))
val receiveOuts: Seq[HtlcTemplate] = htlcs.filter(_.direction == IN).map(htlc => {
HtlcTemplate(htlc, ourFinalKey, theirFinalKey, theirDelay, revocationHash)
})
TxTemplate(inputs, ourOutput, theirOutput, sendOuts, receiveOuts)
}
@ -265,4 +282,31 @@ object Scripts {
tx.txOut.zipWithIndex.find {
case (TxOut(_, script), _) => script == publicKeyScript
} map (_._2)
/**
*
* @param tx
* @return the block height before which this tx cannot be published
*/
def cltvTimeout(tx: Transaction): Long = {
require(tx.lockTime <= LockTimeThreshold)
tx.lockTime
}
/**
*
* @param tx
* @return the number of confirmations of the tx parent before which it can be published
*/
def csvTimeout(tx: Transaction): Long = {
def sequenceToBlockHeight(sequence: Long): Long = {
if ((sequence & TxIn.SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) 0
else {
require((sequence & TxIn.SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times")
sequence & TxIn.SEQUENCE_LOCKTIME_MASK
}
}
if (tx.version < 2) 0
else tx.txIn.map(_.sequence).map(sequenceToBlockHeight).max
}
}

View file

@ -10,7 +10,7 @@
<logger name="com.ning.http" level="INFO"/>
<root level="DEBUG">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>

View file

@ -38,37 +38,4 @@ class ConversionSpec extends FunSuite {
assert(sig1.take(32) === sig.take(32))
}
}
ignore("signature compatibility tests") {
val priv: BinaryData = "a9fa8bb22b3ad32682beac860037ea1c1ea86c4598fdffbeb780e70fc19498bd"
val datas: Seq[BinaryData] = Seq(
"8d94049b0beeab2651e4c85db4c132ff38c540129b2f2a739c05c627a2520b25",
"a397a25553bef1fcf9796b521413e9e22d518e1f56085727a705d4d052827775",
"1b994aed583d6a5236d5244a688ead955f3c35b5c48cdd6c11323de2b4b459cf",
"ce233d27dfa7f996fc1ee0662c0e7b8cca30428fbc9f7bced1b8b187ec8ad6bb",
"2e15630e3cdca43a7a0620a7931b34dd4cf5ec889668d668a0096f8e9347c941",
"dbaccf9789f3510579712c0e0d606b59d559e16cc1b95563424471550bba97e6",
"6867fe715b507655c222634f02cea8d8a70a456a431a4d855f3edb6af873d161",
"5ad0533521c98a63ec6db2eebcda4764e50d4ea8271b2f06598af0537dc234d7",
"13878cb3d197183e844aacc0a5f4260b02f3b3a81062afe8eca03c6ae2f0c1f5",
"78cea9c96641086a0c352cb12ad13cabc5ef545553833fbf24fa2b86eb6c7c65")
val protobufs: Seq[BinaryData] = Seq(
"0973d2b3f1c3496ff61122518ec6900e006d1968fd7e30c8cd0b5321670ceec2318563bf29a3faa1441c972eb1316e60e3e734b14ef139db513153a30a6b1d413070761616c78d34",
"09276f16525b6d7365112a331157a23dbfb019ccaf539d0ffda4d121ee1b33d6fd01e0d829e0321e4cc682d7cd31abb31a8703937f5d39091a6bc7e7873fb3418ce60bc8cb852633",
"09ba60e30fc33d205c1151734756a7a49115197a769e564d4235f5213af885d8903e6ca129b27aeb0068a239ab31c285471b79b415b439fef003f37c41a234418adda6d93c961152",
"09d83e91dd59efe17d119c321fa1b1806a1619a190265331dfb19b214a0d61067a809dc729c5fd0a2864f8632e31db3f4f90a1be34aa39bd02c7e27e0bd2de419695d91042897c49",
"0995c6df11611dc0f3112b58a58a8b44e54e19749d5f7c0c14c4f421d985dd793999bcdb298a449e0471fc0710315980e0a69e06478439a18193827e61b21c4113c6127f48503603",
"09edf5abe5d9aff5341165455dcd4e04b21e19d215046818a95af821a6d565d6c31b494d29d332309c6b14b3613155e0a65b6aa40fb43907d0ff9da5d0deaa413d74e2d45240673a",
"099653a6e8a8b2e12511c6576d324e0a5cf4199da11f503691007321ddc6dec5ac6e4d9c2979daa8c921a492a231b1038e0495e6f82e3906317339cf830b9e41b455931e6c1a8332",
"095d14f187c26a28561120c8124a4fc1e5ca197d875b0333f3bcdb21f5fabdacad52cafe29de0d9e0c42bbcee631cd5fd7af87df4c47390c753ad7f5b8cf6141b9a6e96cd7527219",
"0939a88ec980b80eaa11565a7144e3d79dcb1944ef30b7ddc096b021a59a7e8e6fac0402296dc9b21373cff9bd312d0ee2005612e18c39e488ad0930cdc4024108a009fb8d0a8f5d",
"094d4a73c5c030b89c115578af9df0ae8c1f196ad4a2c0bb6bb35b21ce47040662f90d6f29d41d678a79235752312827856672cd6fec39cb9082b45d2fa4b5418f1bbea644a15229"
)
for (i <- 0 until datas.size) {
val sig = signature2bin(lightning.signature.parseFrom(protobufs(i)))
assert(Crypto.verifySignature(datas(i), sig, Crypto.publicKeyFromPrivateKey(priv)))
}
}
}

View file

@ -1,5 +1,6 @@
package fr.acinq.eclair
import akka.actor.ActorRef
import fr.acinq.bitcoin.{BinaryData, BitcoinJsonRPCClient, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.ExtendedBitcoinClient
import fr.acinq.eclair.channel.Scripts
@ -9,7 +10,7 @@ import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 26/04/2016.
*/
class TestBitcoinClient extends ExtendedBitcoinClient(new BitcoinJsonRPCClient("", "", "", 0)) {
class TestBitcoinClient(probe: Option[ActorRef]= None) extends ExtendedBitcoinClient(new BitcoinJsonRPCClient("", "", "", 0)) {
client.client.close()
@ -22,7 +23,10 @@ class TestBitcoinClient extends ExtendedBitcoinClient(new BitcoinJsonRPCClient("
Future.successful((anchorTx, 0))
}
override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = Future.successful(tx.txid.toString())
override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = {
probe.map(_ ! tx)
Future.successful(tx.txid.toString())
}
override def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] = Future.successful(Some(10))

View file

@ -19,7 +19,7 @@ object TestConstants {
object Alice {
val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cQPmcNr6pwBQPyGfab3SksE9nTCtx9ism9T4dkS9dETNU2KKtJHk")
val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cUrAtLtV7GGddqdkhUxnbZVDWGJBTducpPoon3eKp9Vnr1zxs6BG")
val channelParams = OurChannelParams(locktime(Blocks(4)), commitPrivKey, finalPrivKey, 1, 10000, Crypto.sha256("alice-seed".getBytes()), Some(Satoshi(anchorAmount)))
val channelParams = OurChannelParams(locktime(Blocks(300)), commitPrivKey, finalPrivKey, 1, 10000, Crypto.sha256("alice-seed".getBytes()), Some(Satoshi(anchorAmount)))
val finalPubKey = channelParams.finalPubKey
def revocationHash(index: Long) = Helpers.revocationHash(channelParams.shaSeed, index)
@ -42,7 +42,7 @@ object TestConstants {
object Bob {
val (Base58.Prefix.SecretKeyTestnet, commitPrivKey) = Base58Check.decode("cSUwLtdZ2tht9ZmHhdQue48pfe7tY2GT2TGWJDtjoZgo6FHrubGk")
val (Base58.Prefix.SecretKeyTestnet, finalPrivKey) = Base58Check.decode("cPR7ZgXpUaDPA3GwGceMDS5pfnSm955yvks3yELf3wMJwegsdGTg")
val channelParams = OurChannelParams(locktime(Blocks(4)), commitPrivKey, finalPrivKey, 2, 10000, Crypto.sha256("bob-seed".getBytes()), None)
val channelParams = OurChannelParams(locktime(Blocks(350)), commitPrivKey, finalPrivKey, 2, 10000, Crypto.sha256("bob-seed".getBytes()), None)
val finalPubKey = channelParams.finalPubKey
def revocationHash(index: Long) = Helpers.revocationHash(channelParams.shaSeed, index)

View file

@ -0,0 +1,87 @@
package fr.acinq.eclair.channel
import fr.acinq.bitcoin._
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair._
import lightning.locktime.Locktime.Blocks
import lightning.{locktime, routing, update_add_htlc}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@RunWith(classOf[JUnitRunner])
class HandleTheirCurrentCommitTxSpec extends FunSuite {
def signAndReceiveRevocation(sender: Commitments, receiver: Commitments): (Commitments, Commitments) = {
val (sender1, commit1) = Commitments.sendCommit(sender)
val (receiver1, rev1) = Commitments.receiveCommit(receiver, commit1)
val sender2 = Commitments.receiveRevocation(sender1, rev1)
(sender2, receiver1)
}
def addHtlc(sender: Commitments, receiver: Commitments, htlc: update_add_htlc): (Commitments, Commitments) = {
(Commitments.sendAdd(sender, CMD_ADD_HTLC(id = Some(htlc.id), amountMsat = htlc.amountMsat, rHash = htlc.rHash, expiry = htlc.expiry))._1, Commitments.receiveAdd(receiver, htlc))
}
test("claim received htlcs in their current commit tx") {
val alice = Alice.commitments
val bob = Bob.commitments
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
val R1: BinaryData = "0202030405060708010203040506070801020304050607080102030405060708"
val H1 = Crypto.sha256(R1)
val (alice0, bob0) = addHtlc(alice, bob, update_add_htlc(1, 70000000, H, locktime(Blocks(400)), routing.defaultInstance))
val (alice1, bob1) = addHtlc(alice0, bob0, update_add_htlc(2, 80000000, H1, locktime(Blocks(350)), routing.defaultInstance))
val (alice2, bob2) = signAndReceiveRevocation(alice1, bob1)
val (bob3, alice3) = signAndReceiveRevocation(bob2, alice2)
// Alice publishes her current commit tx
val tx = alice3.ourCommit.publishableTx
// suppose we have the payment preimage, what do we do ?
val (bob4, _) = Commitments.sendFulfill(bob3, CMD_FULFILL_HTLC(1, R))
val (bob5, _) = Commitments.sendFulfill(bob4, CMD_FULFILL_HTLC(2, R1))
// we're Bob. Check that our view of Alice's commit tx is right
val theirTxTemplate = Commitments.makeTheirTxTemplate(bob5)
val theirTx = theirTxTemplate.makeTx
assert(theirTx.txOut === tx.txOut)
val Seq(tx1, tx2) = Helpers.claimReceivedHtlcs(tx, theirTxTemplate, bob5)
assert(tx1.txIn.length == 1 && tx1.txOut.length == 1 && tx2.txIn.length == 1 && tx2.txOut.length == 1)
assert(Set(tx1.txOut(0).amount, tx2.txOut(0).amount) == Set(Satoshi(70000), Satoshi(80000)))
Transaction.correctlySpends(tx1, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
Transaction.correctlySpends(tx2, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
test("claim sent htlcs in their current commit tx") {
val alice = Alice.commitments
val bob = Bob.commitments
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
val R1: BinaryData = "0202030405060708010203040506070801020304050607080102030405060708"
val H1 = Crypto.sha256(R1)
val (alice0, bob0) = addHtlc(alice, bob, update_add_htlc(1, 70000000, H, locktime(Blocks(400)), routing.defaultInstance))
val (alice1, bob1) = addHtlc(alice0, bob0, update_add_htlc(1, 80000000, H1, locktime(Blocks(350)), routing.defaultInstance))
val (alice2, bob2) = signAndReceiveRevocation(alice1, bob1)
val (bob3, alice3) = signAndReceiveRevocation(bob2, alice2)
// Bob publishes his current commit tx
val tx = bob3.ourCommit.publishableTx
// we're Alice. Check that our view of Bob's commit tx is right
val theirTxTemplate = Commitments.makeTheirTxTemplate(alice3)
val theirTx = theirTxTemplate.makeTx
assert(theirTx.txOut === tx.txOut)
val Seq(tx1, tx2) = Helpers.claimSentHtlcs(tx, theirTxTemplate, alice3)
assert(tx1.txIn.length == 1 && tx1.txOut.length == 1 && tx2.txIn.length == 1 && tx2.txOut.length == 1)
assert(Set(tx1.txOut(0).amount, tx2.txOut(0).amount) == Set(Satoshi(70000), Satoshi(80000)))
Transaction.correctlySpends(tx1, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
Transaction.correctlySpends(tx2, Seq(tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
}

View file

@ -54,10 +54,8 @@ class StealRevokedCommitmentSpec extends FunSuite {
// now what if Alice published a revoked commit tx ?
Seq(alice1, alice2, alice3, alice4).map(alice => {
val stealTx = bob5.txDb.get(alice.ourCommit.publishableTx.txid)
Transaction.correctlySpends(stealTx.get, Seq(alice.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
})
val stealTx = bob5.txDb.get(alice4.ourCommit.publishableTx.txid)
Transaction.correctlySpends(stealTx.get, Seq(alice4.ourCommit.publishableTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// but we cannot steal Alice's current commit tx
assert(bob5.txDb.get(alice5.ourCommit.publishableTx.txid) == None)

View file

@ -27,7 +27,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
bob ! SubscribeTransitionCallBack(monitorB.ref)
val CurrentState(_, OPEN_WAIT_FOR_OPEN_NOANCHOR) = monitorB.expectMsgClass(classOf[CurrentState[_]])
pipe !(alice, bob) // this starts the communication between alice and bob
pipe ! (alice, bob) // this starts the communication between alice and bob
within(30 seconds) {
@ -46,7 +46,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
}
test("create and fulfill HTLCs") { case (alice, bob, pipe) =>
pipe !(alice, bob) // this starts the communication between alice and bob
pipe ! (alice, bob) // this starts the communication between alice and bob
within(30 seconds) {
@ -56,7 +56,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(4)))
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(400)))
Thread.sleep(100)
(alice.stateData: @unchecked) match {
@ -108,7 +108,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
val R1 = Crypto.sha256(H)
val H1 = Crypto.sha256(R1)
alice ! CMD_ADD_HTLC(60000000, H1, locktime(Blocks(4)))
alice ! CMD_ADD_HTLC(60000000, H1, locktime(Blocks(400)))
Thread.sleep(500)
(alice.stateData: @unchecked) match {
@ -160,7 +160,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
}
test("close channel starting with no HTLC") { case (alice, bob, pipe) =>
pipe !(alice, bob) // this starts the communication between alice and bob
pipe ! (alice, bob) // this starts the communication between alice and bob
within(30 seconds) {
@ -177,7 +177,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
test("close channel with pending htlcs") { case (alice, bob, pipe) =>
within(30 seconds) {
pipe !(alice, bob) // this starts the communication between alice and bob
pipe ! (alice, bob) // this starts the communication between alice and bob
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
@ -196,7 +196,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(4)))
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(400)))
alice ! CMD_SIGN
bob ! CMD_SIGN
alice ! CMD_CLOSE(None)
@ -219,7 +219,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
}
test("steal revoked commit tx") { case (alice, bob, pipe) =>
pipe !(alice, bob) // this starts the communication between alice and bob
pipe ! (alice, bob) // this starts the communication between alice and bob
within(30 seconds) {
@ -229,7 +229,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
val R: BinaryData = "0102030405060708010203040506070801020304050607080102030405060708"
val H = Crypto.sha256(R)
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(4)))
alice ! CMD_ADD_HTLC(60000000, H, locktime(Blocks(400)))
alice ! CMD_SIGN
Thread.sleep(500)
bob ! CMD_SIGN
@ -246,7 +246,7 @@ class NominalChannelSpec extends BaseChannelTestClass {
alice ! CMD_SIGN
Thread.sleep(500)
bob !(BITCOIN_ANCHOR_SPENT, commitTx)
bob ! (BITCOIN_ANCHOR_SPENT, commitTx)
awaitCond(bob.stateName == CLOSING)
}
}

View file

@ -0,0 +1,51 @@
package fr.acinq.eclair.channel.simulator.states
import akka.testkit.{TestFSMRef, TestKitBase, TestProbe}
import fr.acinq.bitcoin.Crypto
import fr.acinq.eclair.channel._
import fr.acinq.eclair._
import lightning.locktime.Locktime.Blocks
import lightning._
import scala.util.Random
/**
* Created by PM on 23/08/2016.
*/
trait StateTestsHelperMethods extends TestKitBase {
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(1440))))
sender.expectMsg("ok")
val htlc = s2r.expectMsgType[update_add_htlc]
s2r.forward(r)
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.theirChanges.proposed.contains(htlc))
(R, htlc)
}
def fulfillHtlc(id: Long, R: rval, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe) = {
val sender = TestProbe()
sender.send(s, CMD_FULFILL_HTLC(id, R))
sender.expectMsg("ok")
val fulfill = s2r.expectMsgType[update_fulfill_htlc]
s2r.forward(r)
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.theirChanges.proposed.contains(fulfill))
}
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)
}
}

View file

@ -8,6 +8,7 @@ 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.simulator.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, Data, State, _}
import lightning._
import lightning.locktime.Locktime.Blocks
@ -16,13 +17,12 @@ 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.
*/
@RunWith(classOf[JUnitRunner])
class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll with StateTestsHelperMethods {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
@ -71,37 +71,12 @@ 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 initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe()
val h = sha256_hash(1, 2, 3, 4)
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(500000, h, locktime(Blocks(144))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
assert(htlc.id == 1 && htlc.rHash == h)
@ -114,7 +89,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(Int.MaxValue, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(Int.MaxValue, sha256_hash(1, 1, 1, 1), locktime(Blocks(144))))
sender.expectMsg("insufficient funds (available=1000000000 msat)")
}
}
@ -122,11 +97,11 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 1/2)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(144))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(144))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(144))))
sender.expectMsg("insufficient funds (available=0 msat)")
}
}
@ -134,19 +109,32 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(144))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(144))))
sender.expectMsg("ok")
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3))))
sender.send(alice, CMD_ADD_HTLC(500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(144))))
sender.expectMsg("insufficient funds (available=400000000 msat)")
}
}
test("recv CMD_ADD_HTLC (while waiting for close_clearing)") { case (alice, _, alice2bob, _, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(alice, CMD_CLOSE(None))
alice2bob.expectMsgType[close_clearing]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].ourClearing.isDefined)
// actual test starts here
sender.send(alice, CMD_ADD_HTLC(300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(144))))
sender.expectMsg("cannot send new htlcs, closing in progress")
}
}
test("recv update_add_htlc") { case (_, bob, alice2bob, _, _, _) =>
within(30 seconds) {
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
val htlc = update_add_htlc(42, 150, sha256_hash(1, 2, 3, 4), locktime(Blocks(3)), routing(ByteString.EMPTY))
val htlc = update_add_htlc(42, 150, sha256_hash(1, 2, 3, 4), locktime(Blocks(144)), routing(ByteString.EMPTY))
bob ! htlc
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(theirChanges = initialData.commitments.theirChanges.copy(proposed = initialData.commitments.theirChanges.proposed :+ htlc))))
}
@ -155,7 +143,7 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_add_htlc (insufficient funds)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val htlc = update_add_htlc(42, Int.MaxValue, sha256_hash(1, 2, 3, 4), locktime(Blocks(3)), routing(ByteString.EMPTY))
val htlc = update_add_htlc(42, Int.MaxValue, sha256_hash(1, 2, 3, 4), locktime(Blocks(144)), routing(ByteString.EMPTY))
alice2bob.forward(bob, htlc)
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
@ -167,9 +155,9 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_add_htlc (insufficient funds w/ pending htlcs 1/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice2bob.forward(bob, update_add_htlc(42, 500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(42, 500000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(144)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 500000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(144)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(144)), routing(ByteString.EMPTY)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
@ -180,9 +168,9 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
test("recv update_add_htlc (insufficient funds w/ pending htlcs 2/2)") { case (_, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice2bob.forward(bob, update_add_htlc(42, 300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(3)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(42, 300000000, sha256_hash(1, 1, 1, 1), locktime(Blocks(144)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(43, 300000000, sha256_hash(2, 2, 2, 2), locktime(Blocks(144)), routing(ByteString.EMPTY)))
alice2bob.forward(bob, update_add_htlc(44, 500000000, sha256_hash(3, 3, 3, 3), locktime(Blocks(144)), routing(ByteString.EMPTY)))
bob2alice.expectMsgType[error]
awaitCond(bob.stateName == CLOSING)
bob2blockchain.expectMsg(Publish(tx))
@ -250,12 +238,12 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
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.send(alice, CMD_ADD_HTLC(5000000, h, locktime(Blocks(144))))
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.send(alice, CMD_ADD_HTLC(5000000, h, locktime(Blocks(144))))
sender.expectMsg("ok")
val htlc2 = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
@ -532,6 +520,9 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
/**
* see https://github.com/ElementsProject/lightning/issues/29
*/
ignore("recv close_clearing (with unacked received htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -543,6 +534,9 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
/**
* see https://github.com/ElementsProject/lightning/issues/29
*/
ignore("recv close_clearing (with unacked sent htlcs)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -567,6 +561,57 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
test("recv BITCOIN_ANCHOR_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) =>
within(30 seconds) {
val sender = TestProbe()
val (r1, htlc1) = addHtlc(300000000, alice, bob, alice2bob, bob2alice) // id 1
val (r2, htlc2) = addHtlc(200000000, alice, bob, alice2bob, bob2alice) // id 2
val (r3, htlc3) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) // id 3
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(1, r1, bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
val (r4, htlc4) = addHtlc(150000000, bob, alice, bob2alice, alice2bob) // id 1
val (r5, htlc5) = addHtlc(120000000, bob, alice, bob2alice, alice2bob) // id 2
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(2, r2, bob, alice, bob2alice, alice2bob)
fulfillHtlc(1, r4, alice, bob, alice2bob, bob2alice)
// at this point here is the situation from alice pov and what she should do :
// balances :
// alice's balance : 400 000 000 => nothing to do
// bob's balance : 30 000 000 => nothing to do
// htlcs :
// alice -> bob : 200 000 000 (bob has the r) => if bob does not use the r, wait for the timeout and spend
// alice -> bob : 100 000 000 (bob does not have the r) => wait for the timeout and spend
// bob -> alice : 150 000 000 (alice has the r) => spend immediately using the r
// bob -> alice : 120 000 000 (alice does not have the r) => nothing to do, bob will get his money back after the timeout
// bob publishes his current commit tx
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
assert(bobCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs
alice ! (BITCOIN_ANCHOR_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
val amountClaimed = (for (i <- 0 until 3) yield {
// alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc
val claimHtlcTx = alice2blockchain.expectMsgType[PublishAsap].tx
assert(claimHtlcTx.txIn.size == 1)
val previousOutputs = Map(claimHtlcTx.txIn(0).outPoint -> bobCommitTx.txOut(claimHtlcTx.txIn(0).outPoint.index.toInt))
Transaction.correctlySpends(claimHtlcTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
assert(claimHtlcTx.txOut.size == 1)
claimHtlcTx.txOut(0).amount
}).sum
assert(amountClaimed == Satoshi(450000))
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx))
}
}
test("recv BITCOIN_ANCHOR_SPENT (revoked commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
@ -618,13 +663,52 @@ class NormalStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuite
}
}
test("recv error") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv error") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
val (r1, htlc1) = addHtlc(300000000, alice, bob, alice2bob, bob2alice) // id 1
val (r2, htlc2) = addHtlc(200000000, alice, bob, alice2bob, bob2alice) // id 2
val (r3, htlc3) = addHtlc(100000000, alice, bob, alice2bob, bob2alice) // id 3
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(1, r1, bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
val (r4, htlc4) = addHtlc(150000000, bob, alice, bob2alice, alice2bob) // id 1
val (r5, htlc5) = addHtlc(120000000, bob, alice, bob2alice, alice2bob) // id 2
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
fulfillHtlc(2, r2, bob, alice, bob2alice, alice2bob)
fulfillHtlc(1, r4, alice, bob, alice2bob, bob2alice)
// at this point here is the situation from alice pov and what she should do :
// balances :
// alice's balance : 400 000 000 => nothing to do
// bob's balance : 30 000 000 => nothing to do
// htlcs :
// alice -> bob : 200 000 000 (bob has the r) => if bob does not use the r, wait for the timeout and spend
// alice -> bob : 100 000 000 (bob does not have the r) => wait for the timeout and spend
// bob -> alice : 150 000 000 (alice has the r) => spend immediately using the r
// bob -> alice : 120 000 000 (alice does not have the r) => nothing to do, bob will get his money back after the timeout
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice ! error(Some("oops"))
alice2blockchain.expectMsg(Publish(aliceCommitTx))
assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
val amountClaimed = (for (i <- 0 until 3) yield {
// alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the htlc
val claimHtlcTx = alice2blockchain.expectMsgType[PublishAsap].tx
assert(claimHtlcTx.txIn.size == 1)
val previousOutputs = Map(claimHtlcTx.txIn(0).outPoint -> aliceCommitTx.txOut(claimHtlcTx.txIn(0).outPoint.index.toInt))
Transaction.correctlySpends(claimHtlcTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
assert(claimHtlcTx.txOut.size == 1)
claimHtlcTx.txOut(0).amount
}).sum
assert(amountClaimed == Satoshi(450000))
awaitCond(alice.stateName == CLOSING)
alice2blockchain.expectMsg(Publish(tx))
alice2blockchain.expectMsgType[WatchConfirmed]
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
}
}

View file

@ -3,10 +3,11 @@ package fr.acinq.eclair.channel.simulator.states.f
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.TestConstants.{Alice, Bob}
import fr.acinq.eclair.{TestBitcoinClient, _}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.simulator.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, Data, State, _}
import lightning._
import lightning.locktime.Locktime.Blocks
@ -20,7 +21,7 @@ import scala.concurrent.duration._
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll {
class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll with StateTestsHelperMethods {
type FixtureParam = Tuple6[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe]
@ -64,14 +65,22 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
// note : alice is funder and bob is fundee, so alice has all the money
val sender = TestProbe()
// alice sends an HTLC to bob
val r: rval = rval(1, 2, 3, 4)
val h: sha256_hash = Crypto.sha256(r)
val amount = 500000
sender.send(alice, CMD_ADD_HTLC(amount, h, locktime(Blocks(3))))
val r1: rval = rval(1, 1, 1, 1)
val h1: sha256_hash = Crypto.sha256(r1)
val amount1 = 300000000
sender.send(alice, CMD_ADD_HTLC(amount1, h1, locktime(Blocks(1440))))
sender.expectMsg("ok")
val htlc = alice2bob.expectMsgType[update_add_htlc]
val htlc1 = alice2bob.expectMsgType[update_add_htlc]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc :: Nil)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == htlc1 :: Nil)
val r2: rval = rval(2, 2, 2, 2)
val h2: sha256_hash = Crypto.sha256(r2)
val amount2 = 200000000
sender.send(alice, CMD_ADD_HTLC(amount2, h2, locktime(Blocks(1440))))
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)
// alice signs
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -79,7 +88,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
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)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.proposed == Nil && bob.stateData.asInstanceOf[DATA_NORMAL].commitments.theirChanges.acked == htlc1 :: htlc2 :: Nil)
// alice initiates a closing
sender.send(alice, CMD_CLOSE(None))
alice2bob.expectMsgType[close_clearing]
@ -99,7 +108,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
within(30 seconds) {
val sender = TestProbe()
val initialState = bob.stateData.asInstanceOf[DATA_CLEARING]
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 1, 1, 1)))
sender.expectMsg("ok")
val fulfill = bob2alice.expectMsgType[update_fulfill_htlc]
awaitCond(bob.stateData == initialState.copy(commitments = initialState.commitments.copy(ourChanges = initialState.commitments.ourChanges.copy(initialState.commitments.ourChanges.proposed :+ fulfill))))
@ -130,11 +139,9 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_CLEARING]
val fulfill = update_fulfill_htlc(1, rval(1, 2, 3, 4))
val fulfill = update_fulfill_htlc(1, rval(1, 1, 1, 1))
sender.send(alice, fulfill)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(theirChanges = initialState.commitments.theirChanges.copy(initialState.commitments.theirChanges.proposed :+ fulfill)),
downstreams = Map()))
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments == initialState.commitments.copy(theirChanges = initialState.commitments.theirChanges.copy(initialState.commitments.theirChanges.proposed :+ fulfill)))
}
}
@ -190,9 +197,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
val initialState = alice.stateData.asInstanceOf[DATA_CLEARING]
val fail = update_fail_htlc(1, fail_reason(ByteString.copyFromUtf8("some reason")))
sender.send(alice, fail)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(theirChanges = initialState.commitments.theirChanges.copy(initialState.commitments.theirChanges.proposed :+ fail)),
downstreams = Map()))
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments == initialState.commitments.copy(theirChanges = initialState.commitments.theirChanges.copy(initialState.commitments.theirChanges.proposed :+ fail)))
}
}
@ -212,7 +217,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
within(30 seconds) {
val sender = TestProbe()
// we need to have something to sign so we first send a fulfill and acknowledge (=sign) it
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 1, 1, 1)))
bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
@ -253,7 +258,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
test("recv update_commit") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 1, 1, 1)))
sender.expectMsg("ok")
bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
@ -290,39 +295,57 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
}
}
ignore("recv update_revocation (no more htlcs)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv update_revocation (with remaining htlcs on both sides)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
fulfillHtlc(2, rval(2, 2, 2, 2), bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
val sender = TestProbe()
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[update_commit]
bob2alice.forward(alice)
alice2bob.expectMsgType[update_revocation]
awaitCond(bob.stateData.asInstanceOf[DATA_CLEARING].commitments.theirNextCommitInfo.isLeft)
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
awaitCond(bob.stateName == DATA_NEGOTIATING)
bob2alice.expectMsgType[update_revocation]
// actual test starts here
bob2alice.forward(bob)
assert(alice.stateName == CLEARING)
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.spec.htlcs.size == 1)
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments.theirCommit.spec.htlcs.size == 2)
}
}
test("recv update_revocation (with remaining htlcs)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
test("recv update_revocation (with remaining htlcs on one side)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
fulfillHtlc(1, rval(1, 1, 1, 1), bob, alice, bob2alice, alice2bob)
fulfillHtlc(2, rval(2, 2, 2, 2), bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
val sender = TestProbe()
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[update_commit]
bob2alice.forward(alice)
alice2bob.expectMsgType[update_revocation]
awaitCond(bob.stateData.asInstanceOf[DATA_CLEARING].commitments.theirNextCommitInfo.isLeft)
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_CLEARING].commitments.theirNextCommitInfo.isRight)
bob2alice.expectMsgType[update_revocation]
// actual test starts here
bob2alice.forward(bob)
assert(alice.stateName == CLEARING)
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.spec.htlcs.isEmpty)
awaitCond(alice.stateData.asInstanceOf[DATA_CLEARING].commitments.theirCommit.spec.htlcs.size == 2)
}
}
test("recv update_revocation (no more htlcs on either side)") { case (alice, bob, alice2bob, bob2alice, _, bob2blockchain) =>
within(30 seconds) {
fulfillHtlc(1, rval(1, 1, 1, 1), bob, alice, bob2alice, alice2bob)
fulfillHtlc(2, rval(2, 2, 2, 2), bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
val sender = TestProbe()
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
alice2bob.expectMsgType[update_commit]
alice2bob.forward(bob)
bob2alice.expectMsgType[update_revocation]
// actual test starts here
bob2alice.forward(alice)
awaitCond(alice.stateName == NEGOTIATING)
}
}
@ -330,7 +353,7 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
within(30 seconds) {
val tx = bob.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
val sender = TestProbe()
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 2, 3, 4)))
sender.send(bob, CMD_FULFILL_HTLC(1, rval(1, 1, 1, 1)))
sender.expectMsg("ok")
bob2alice.expectMsgType[update_fulfill_htlc]
bob2alice.forward(alice)
@ -361,22 +384,68 @@ class ClearingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSui
}
}
ignore("recv BITCOIN_ANCHOR_SPENT") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv BITCOIN_ANCHOR_SPENT (their commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
alice ! (BITCOIN_ANCHOR_SPENT, null)
alice2bob.expectMsgType[error]
// TODO
// bob publishes his current commit tx, which contains two pending htlcs alice->bob
val bobCommitTx = bob.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
assert(bobCommitTx.txOut.size == 3) // one main outputs (bob has zero) and 2 pending htlcs
alice ! (BITCOIN_ANCHOR_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
val amountClaimed = (for (i <- 0 until 2) yield {
val claimHtlcTx = alice2blockchain.expectMsgType[PublishAsap].tx
assert(claimHtlcTx.txIn.size == 1)
val previousOutputs = Map(claimHtlcTx.txIn(0).outPoint -> bobCommitTx.txOut(claimHtlcTx.txIn(0).outPoint.index.toInt))
Transaction.correctlySpends(claimHtlcTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
assert(claimHtlcTx.txOut.size == 1)
claimHtlcTx.txOut(0).amount
}).sum
assert(amountClaimed == Satoshi(500000))
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].theirCommitPublished == Some(bobCommitTx))
}
}
test("recv error") { case (alice, _, alice2bob, bob2alice, alice2blockchain, _) =>
test("recv BITCOIN_ANCHOR_SPENT (revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
alice ! error(Some("oops"))
awaitCond(alice.stateName == CLOSING)
alice2blockchain.expectMsg(Publish(tx))
val revokedTx = bob.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
// bob fulfills one of the pending htlc (just so that he can have a new sig)
fulfillHtlc(1, rval(1, 1, 1, 1), bob, alice, bob2alice, alice2bob)
// alice signs
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
// bob now has a new commitment tx
// bob published the revoked tx
alice ! (BITCOIN_ANCHOR_SPENT, revokedTx)
alice2bob.expectMsgType[error]
val punishTx = alice2blockchain.expectMsgType[Publish].tx
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateName == CLOSING)
Transaction.correctlySpends(punishTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// one main output + 2 htlc
assert(revokedTx.txOut.size == 3)
// the punishment tx consumes all output but ours (which already goes to our final key)
assert(punishTx.txIn.size == 2)
assert(punishTx.txOut == Seq(TxOut(Satoshi(500000), Script.write(Scripts.pay2wpkh(Alice.finalPubKey)))))
}
}
test("recv error") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_CLEARING].commitments.ourCommit.publishableTx
alice ! error(Some("oops"))
alice2blockchain.expectMsg(Publish(aliceCommitTx))
assert(aliceCommitTx.txOut.size == 1) // only one main output
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
alice2blockchain.expectNoMsg()
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
}
}

View file

@ -0,0 +1,304 @@
package fr.acinq.eclair.channel.simulator.states.h
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestFSMRef, TestKit, TestProbe}
import fr.acinq.bitcoin.{Satoshi, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.simulator.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{BITCOIN_ANCHOR_DEPTHOK, Data, State, _}
import fr.acinq.eclair.{TestBitcoinClient, _}
import lightning._
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, fixture}
import scala.concurrent.duration._
/**
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class ClosingStateSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike with BeforeAndAfterAll with StateTestsHelperMethods {
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, TestProbe, List[Transaction]]
override def withFixture(test: OneArgTest) = {
val alice2bob = TestProbe()
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val blockchainA = TestActorRef(new PollingWatcher(new TestBitcoinClient()))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
// note that alice.initialFeeRate != bob.initialFeeRate
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams.copy(initialFeeRate = 20000), "A"))
alice2bob.expectMsgType[open_channel]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_channel]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[MakeAnchor]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_anchor]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_commit_sig]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[Publish]
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[WatchSpent]
bob ! BITCOIN_ANCHOR_DEPTHOK
bob2blockchain.expectMsgType[WatchLost]
bob2alice.expectMsgType[open_complete]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchLost]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
// note : alice is funder and bob is fundee, so alice has all the money
val bobCommitTxes: List[Transaction] = (for (amt <- List(100000000, 200000000, 300000000)) yield {
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob)
sign(bob, alice, bob2alice, alice2bob)
sign(alice, bob, alice2bob, bob2alice)
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
bobCommitTx1 :: bobCommitTx2 :: Nil
}).flatten
val sender = TestProbe()
// alice initiates a closing
sender.send(alice, CMD_CLOSE(None))
alice2bob.expectMsgType[close_clearing]
alice2bob.forward(bob)
bob2alice.expectMsgType[close_clearing]
bob2alice.forward(alice)
// agreeing on a closing fee
var aliceCloseFee, bobCloseFee = 0L
do {
aliceCloseFee = alice2bob.expectMsgType[close_signature].closeFee
alice2bob.forward(bob)
bobCloseFee = bob2alice.expectMsgType[close_signature].closeFee
bob2alice.forward(alice)
} while (aliceCloseFee != bobCloseFee)
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[Publish]
bob2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateName == CLOSING)
awaitCond(bob.stateName == CLOSING)
// both nodes are now in CLOSING state with a mutual close tx pending for confirmation
test((alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, bobCommitTxes))
}
override def afterAll {
TestKit.shutdownActorSystem(system)
}
test("recv BITCOIN_CLOSE_DONE") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
alice ! BITCOIN_CLOSE_DONE
awaitCond(alice.stateName == CLOSED)
}
}
test("recv BITCOIN_ANCHOR_SPENT (our commit)") { case (_, _, _, _, _, _, _) =>
within(30 seconds) {
// this test needs a specific intialization because we need to have published our own commitment tx (that's why ignored fixture args)
// to do that alice will receive an error packet when in NORMAL state, which will make her publish her commit tx and then reach CLOSING state
val alice2bob = TestProbe()
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val blockchainA = TestActorRef(new PollingWatcher(new TestBitcoinClient()))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
// note that alice.initialFeeRate != bob.initialFeeRate
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams.copy(initialFeeRate = 20000), "A"))
alice2bob.expectMsgType[open_channel]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_channel]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[MakeAnchor]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_anchor]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_commit_sig]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[Publish]
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[WatchSpent]
bob ! BITCOIN_ANCHOR_DEPTHOK
bob2blockchain.expectMsgType[WatchLost]
bob2alice.expectMsgType[open_complete]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchLost]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice ! error(Some("oops"))
alice2blockchain.expectMsg(Publish(aliceCommitTx))
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
awaitCond(alice.stateName == CLOSING)
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(initialState.ourCommitPublished == Some(aliceCommitTx))
// actual test starts here
alice ! (BITCOIN_ANCHOR_SPENT, aliceCommitTx)
assert(alice.stateData == initialState)
}
}
test("recv BITCOIN_SPEND_OURS_DONE") { case (_, _, _, _, _, _, _) =>
within(30 seconds) {
// this test needs a specific intialization because we need to have published our own commitment tx (that's why ignored fixture args)
// to do that alice will receive an error packet when in NORMAL state, which will make her publish her commit tx and then reach CLOSING state
val alice2bob = TestProbe()
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val blockchainA = TestActorRef(new PollingWatcher(new TestBitcoinClient()))
val bob2blockchain = TestProbe()
val paymentHandler = TestProbe()
// note that alice.initialFeeRate != bob.initialFeeRate
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(alice2bob.ref, alice2blockchain.ref, paymentHandler.ref, Alice.channelParams, "B"))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(bob2alice.ref, bob2blockchain.ref, paymentHandler.ref, Bob.channelParams.copy(initialFeeRate = 20000), "A"))
alice2bob.expectMsgType[open_channel]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_channel]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[MakeAnchor]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_anchor]
alice2bob.forward(bob)
bob2alice.expectMsgType[open_commit_sig]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.forward(blockchainA)
alice2blockchain.expectMsgType[Publish]
alice2blockchain.forward(blockchainA)
bob2blockchain.expectMsgType[WatchConfirmed]
bob2blockchain.expectMsgType[WatchSpent]
bob ! BITCOIN_ANCHOR_DEPTHOK
bob2blockchain.expectMsgType[WatchLost]
bob2alice.expectMsgType[open_complete]
bob2alice.forward(alice)
alice2blockchain.expectMsgType[WatchLost]
alice2blockchain.forward(blockchainA)
alice2bob.expectMsgType[open_complete]
alice2bob.forward(bob)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.ourCommit.publishableTx
alice ! error(Some("oops"))
alice2blockchain.expectMsg(Publish(aliceCommitTx))
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
awaitCond(alice.stateName == CLOSING)
assert(alice.stateData.asInstanceOf[DATA_CLOSING].ourCommitPublished == Some(aliceCommitTx))
// actual test starts here
alice ! BITCOIN_SPEND_OURS_DONE
awaitCond(alice.stateName == CLOSED)
}
}
test("recv BITCOIN_ANCHOR_SPENT (their commit)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxes.last
assert(bobCommitTx.txOut.size == 2) // two main outputs
alice ! (BITCOIN_ANCHOR_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx)))
}
}
test("recv BITCOIN_SPEND_THEIRS_DONE") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes his last current commit tx, the one it had when entering NEGOTIATING state
val bobCommitTx = bobCommitTxes.last
assert(bobCommitTx.txOut.size == 2) // two main outputs
alice ! (BITCOIN_ANCHOR_SPENT, bobCommitTx)
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(theirCommitPublished = Some(bobCommitTx)))
// actual test starts here
alice ! BITCOIN_SPEND_THEIRS_DONE
awaitCond(alice.stateName == CLOSED)
}
}
test("recv BITCOIN_ANCHOR_SPENT (one revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes one of his revoked txes
val bobRevokedTx = bobCommitTxes.head
alice ! (BITCOIN_ANCHOR_SPENT, bobRevokedTx)
// alice publishes and watches the stealing tx
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx)))
}
}
test("recv BITCOIN_ANCHOR_SPENT (multiple revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
// bob publishes multiple revoked txes (last one isn't revoked)
for (bobRevokedTx <- bobCommitTxes.dropRight(1)) {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
alice ! (BITCOIN_ANCHOR_SPENT, bobRevokedTx)
// alice publishes and watches the stealing tx
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = initialState.revokedPublished :+ bobRevokedTx))
}
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedPublished.size == bobCommitTxes.size - 1)
}
}
test("recv BITCOIN_STEAL_DONE (one revoked tx)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, bobCommitTxes) =>
within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
// bob publishes one of his revoked txes
val bobRevokedTx = bobCommitTxes.head
alice ! (BITCOIN_ANCHOR_SPENT, bobRevokedTx)
// alice publishes and watches the stealing tx
alice2blockchain.expectMsgType[Publish]
alice2blockchain.expectMsgType[WatchConfirmed]
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING] == initialState.copy(revokedPublished = Seq(bobRevokedTx)))
// actual test starts here
alice ! BITCOIN_STEAL_DONE
awaitCond(alice.stateName == CLOSED)
}
}
}

View file

@ -114,11 +114,8 @@ class InteroperabilitySpec extends TestKit(ActorSystem("test")) with FunSuiteLik
super.afterAll()
}
def actorPathToChannelId(channelId: BinaryData): ActorPath =
system / "register" / "handler-*" / "channel" / s"*-${channelId}"
def sendCommand(channel_id: String, cmd: Command): Future[String] = {
system.actorSelection(actorPathToChannelId(channel_id)).resolveOne().map(actor => {
def sendCommand(channelId: String, cmd: Command): Future[String] = {
system.actorSelection(Register.actorPathToChannelId(system, channelId)).resolveOne().map(actor => {
actor ! cmd
"ok"
})

View file

@ -41,7 +41,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging
def resolve(x: String) = if (x == "A") a else b
script match {
case offer(x, id, amount, rhash) :: rest =>
resolve(x) ! CMD_ADD_HTLC(amount.toInt, BinaryData(rhash), locktime(Blocks(4)), id = Some(id.toLong))
resolve(x) ! CMD_ADD_HTLC(amount.toInt, BinaryData(rhash), locktime(Blocks(144)), id = Some(id.toLong))
exec(rest, a, b)
case fulfill(x, id, r) :: rest =>
resolve(x) ! CMD_FULFILL_HTLC(id.toInt, BinaryData(r))