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:
commit
b1589a9a5f
22 changed files with 1045 additions and 349 deletions
|
@ -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
|
||||
}
|
||||
))
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<logger name="com.ning.http" level="INFO"/>
|
||||
|
||||
<root level="DEBUG">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Reference in a new issue