mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-20 02:27:32 +01:00
made commitment functions no-op when replaying known messages, improved fuzzy tests
This commit is contained in:
parent
b8fac57ac0
commit
6ebe5b4ff2
@ -8,7 +8,7 @@ import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
|
||||
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
|
||||
import fr.acinq.eclair.crypto.{Generators, ShaChain}
|
||||
import fr.acinq.eclair.payment.Binding
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire._
|
||||
@ -391,26 +391,21 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) if d.unackedShutdown.isDefined =>
|
||||
handleCommandError(sender, new RuntimeException("cannot send new htlcs, closing in progress"))
|
||||
|
||||
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, origin, do_commit), d@DATA_NORMAL(params, commitments, _)) =>
|
||||
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, downstream_opt, do_commit), d@DATA_NORMAL(params, commitments, _)) =>
|
||||
Try(Commitments.sendAdd(commitments, c)) match {
|
||||
case Success((commitments1, add)) =>
|
||||
relayer ! Binding(add, origin)
|
||||
val origin = downstream_opt.map(Relayed(_)).getOrElse(Local(sender))
|
||||
relayer ! Bind(add, origin)
|
||||
if (do_commit) self ! CMD_SIGN
|
||||
handleCommandSuccess(sender, add, d.copy(commitments = commitments1))
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
}
|
||||
|
||||
case Event(add: UpdateAddHtlc, d@DATA_NORMAL(params, commitments, _)) =>
|
||||
if (Commitments.isOldAdd(d.commitments, add)) {
|
||||
log.warning(s"ignoring old add")
|
||||
stay
|
||||
} else {
|
||||
Try(Commitments.receiveAdd(commitments, add)) match {
|
||||
case Success(commitments1) =>
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(commitments1) => stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
}
|
||||
|
||||
case Event(c@CMD_FULFILL_HTLC(id, r, do_commit), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFulfill(d.commitments, c)) match {
|
||||
@ -421,20 +416,13 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
}
|
||||
|
||||
case Event(fulfill@UpdateFulfillHtlc(_, id, r), d@DATA_NORMAL(params, commitments, _)) =>
|
||||
if (Commitments.isOldFulfill(d.commitments, fulfill)) {
|
||||
log.warning(s"ignoring old fulfill")
|
||||
stay
|
||||
} /*else if (!Commitments.isFulfillCrossSigned(d.commitments, fulfill)) {
|
||||
log.warning(s"ignoring fulfill not cross-signed (old?)")
|
||||
stay
|
||||
}*/ else {
|
||||
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
|
||||
case Success((commitments1, htlc)) =>
|
||||
relayer ! (htlc, fulfill)
|
||||
case Success(Right(commitments1)) =>
|
||||
relayer ! ForwardFulfill(fulfill)
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) => stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
}
|
||||
|
||||
case Event(c@CMD_FAIL_HTLC(id, reason, do_commit), d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendFail(d.commitments, c)) match {
|
||||
@ -446,9 +434,10 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
|
||||
case Event(fail@UpdateFailHtlc(_, id, reason), d@DATA_NORMAL(params, _, _)) =>
|
||||
Try(Commitments.receiveFail(d.commitments, fail)) match {
|
||||
case Success((commitments1, htlc)) =>
|
||||
relayer ! (htlc, fail)
|
||||
case Success(Right(commitments1)) =>
|
||||
relayer ! ForwardFail(fail)
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) => stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
|
||||
@ -470,12 +459,8 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
}
|
||||
|
||||
case Event(commit@CommitSig(_, theirSig, theirHtlcSigs), d: DATA_NORMAL) =>
|
||||
if (Commitments.isOldCommit(d.commitments, commit)) {
|
||||
log.warning(s"ignoring old commit")
|
||||
stay
|
||||
} else {
|
||||
Try(Commitments.receiveCommit(d.commitments, commit)) match {
|
||||
case Success((commitments1, revocation)) =>
|
||||
case Success(Right((commitments1, revocation))) =>
|
||||
remote ! revocation
|
||||
log.debug(s"received a new sig, spec:\n${Commitments.specsToString(commitments1)}")
|
||||
if (Commitments.localHasChanges(commitments1)) {
|
||||
@ -484,34 +469,34 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
}
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) =>
|
||||
// this was an old commit, nothing to do
|
||||
stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
}
|
||||
|
||||
case Event(revocation: RevokeAndAck, d: DATA_NORMAL) =>
|
||||
if (Commitments.isOldRevocation(d.commitments, revocation)) {
|
||||
log.warning(s"ignoring old revocation")
|
||||
stay
|
||||
} else {
|
||||
// we received a revocation because we sent a signature
|
||||
// => all our changes have been acked
|
||||
Try(Commitments.receiveRevocation(d.commitments, revocation)) match {
|
||||
case Success(commitments1) =>
|
||||
case Success(Right(commitments1)) =>
|
||||
// we forward HTLCs only when they have been committed by both sides
|
||||
// it always happen when we receive a revocation, because, we always sign our changes before they sign them
|
||||
d.commitments.remoteChanges.signed.collect {
|
||||
case htlc: UpdateAddHtlc =>
|
||||
log.debug(s"relaying $htlc")
|
||||
relayer ! htlc
|
||||
relayer ! ForwardAdd(htlc)
|
||||
}
|
||||
log.debug(s"received a new rev, spec:\n${Commitments.specsToString(commitments1)}")
|
||||
if (Commitments.localHasChanges(commitments1) && d.commitments.remoteNextCommitInfo.left.map(_.reSignAsap) == Left(true)) {
|
||||
self ! CMD_SIGN
|
||||
}
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) =>
|
||||
// this was an old revocation, nothing to do
|
||||
stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
}
|
||||
|
||||
case Event(CMD_CLOSE(ourScriptPubKey_opt), d: DATA_NORMAL) if d.unackedShutdown.isDefined =>
|
||||
handleCommandError(sender, new RuntimeException("closing already in progress"))
|
||||
@ -587,9 +572,10 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
|
||||
case Event(fulfill@UpdateFulfillHtlc(_, id, r), d: DATA_SHUTDOWN) =>
|
||||
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
|
||||
case Success((commitments1, htlc)) =>
|
||||
relayer ! (htlc, fulfill)
|
||||
case Success(Right(commitments1)) =>
|
||||
relayer ! ForwardFulfill(fulfill)
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) => stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
|
||||
@ -601,9 +587,10 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
|
||||
case Event(fail@UpdateFailHtlc(_, id, reason), d: DATA_SHUTDOWN) =>
|
||||
Try(Commitments.receiveFail(d.commitments, fail)) match {
|
||||
case Success((commitments1, htlc)) =>
|
||||
relayer ! (htlc, fail)
|
||||
case Success(Right(commitments1)) =>
|
||||
relayer ! ForwardFail(fail)
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) => stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
|
||||
@ -627,13 +614,13 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
case Event(msg@CommitSig(_, theirSig, theirHtlcSigs), d@DATA_SHUTDOWN(params, commitments, localShutdown, remoteShutdown)) =>
|
||||
// TODO: we might have to propagate htlcs upstream depending on the outcome of https://github.com/ElementsProject/lightning/issues/29
|
||||
Try(Commitments.receiveCommit(d.commitments, msg)) match {
|
||||
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
|
||||
case Success(Right((commitments1, revocation))) if commitments1.hasNoPendingHtlcs =>
|
||||
remote ! revocation
|
||||
val closingSigned = Closing.makeFirstClosingTx(params, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
|
||||
remote ! closingSigned
|
||||
log.debug(s"received a new sig, switching to NEGOTIATING spec:\n${Commitments.specsToString(commitments1)}")
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(params, commitments1, localShutdown, remoteShutdown, closingSigned)
|
||||
case Success((commitments1, revocation)) =>
|
||||
case Success(Right((commitments1, revocation))) =>
|
||||
remote ! revocation
|
||||
if (Commitments.localHasChanges(commitments1)) {
|
||||
// if we have newly acknowledged changes let's sign them
|
||||
@ -642,6 +629,9 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
log.debug(s"received a new sig, spec:\n${Commitments.specsToString(commitments1)}")
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) =>
|
||||
// this was an old commit, nothing to do
|
||||
stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
|
||||
@ -649,18 +639,21 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
// we received a revocation because we sent a signature
|
||||
// => all our changes have been acked
|
||||
Try(Commitments.receiveRevocation(d.commitments, msg)) match {
|
||||
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
|
||||
case Success(Right(commitments1)) if commitments1.hasNoPendingHtlcs =>
|
||||
val closingSigned = Closing.makeFirstClosingTx(params, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
|
||||
|
||||
remote ! closingSigned
|
||||
log.debug(s"received a new rev, switching to NEGOTIATING spec:\n${Commitments.specsToString(commitments1)}")
|
||||
goto(NEGOTIATING) using DATA_NEGOTIATING(params, commitments1, localShutdown, remoteShutdown, closingSigned)
|
||||
case Success(commitments1) =>
|
||||
case Success(Right(commitments1)) =>
|
||||
if (Commitments.localHasChanges(commitments1) && d.commitments.remoteNextCommitInfo.left.map(_.reSignAsap) == Left(true)) {
|
||||
self ! CMD_SIGN
|
||||
}
|
||||
log.debug(s"received a new rev, spec:\n${Commitments.specsToString(commitments1)}")
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Success(Left(_)) =>
|
||||
// this was an old revocation, nothing to do
|
||||
stay
|
||||
case Failure(cause) => handleLocalError(cause, d)
|
||||
}
|
||||
|
||||
@ -814,9 +807,9 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
|
||||
case Event(INPUT_RECONNECTED(r), d@DATA_NORMAL(_, commitments, _)) =>
|
||||
remote = r
|
||||
log.info(s"resuming with ${Commitments.toString(commitments)}")
|
||||
log.info(s"resuming with ${Commitments.changesToString(commitments)}")
|
||||
//val resend = commitments.unackedMessages.filterNot(_.isInstanceOf[RevokeAndAck])
|
||||
val resend = commitments.unackedMessages//.filterNot(_.isInstanceOf[RevokeAndAck])
|
||||
val resend = commitments.unackedMessages //.filterNot(_.isInstanceOf[RevokeAndAck])
|
||||
log.info(s"re-sending: ${resend.map(Commitments.msg2String(_)).mkString(" ")}")
|
||||
resend.foreach(remote ! _)
|
||||
if (Commitments.localHasChanges(commitments)) {
|
||||
@ -825,11 +818,12 @@ class Channel(val r: ActorRef, val blockchain: ActorRef, router: ActorRef, relay
|
||||
}
|
||||
goto(NORMAL)
|
||||
|
||||
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, origin, do_commit), d@DATA_NORMAL(params, commitments, _)) =>
|
||||
case Event(c@CMD_ADD_HTLC(amountMsat, rHash, expiry, route, downstream_opt, do_commit), d@DATA_NORMAL(params, commitments, _)) =>
|
||||
log.info(s"we are disconnected so we just include the add in our commitments")
|
||||
Try(Commitments.sendAdd(commitments, c)) match {
|
||||
case Success((commitments1, add)) =>
|
||||
relayer ! Binding(add, origin)
|
||||
val origin = downstream_opt.map(Relayed(_)).getOrElse(Local(sender))
|
||||
relayer ! Bind(add, origin)
|
||||
sender ! "ok"
|
||||
stay using d.copy(commitments = commitments1)
|
||||
case Failure(cause) => handleCommandError(sender, cause)
|
||||
|
@ -3,10 +3,10 @@ package fr.acinq.eclair.channel
|
||||
import akka.actor.ActorRef
|
||||
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
|
||||
import fr.acinq.bitcoin.{BinaryData, ScriptElt, Transaction}
|
||||
import fr.acinq.eclair.payment.{Local, Origin}
|
||||
import fr.acinq.eclair.payment.{Local, Origin, Relayed}
|
||||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ClosingSigned, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ClosingSigned, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
|
||||
|
||||
/**
|
||||
@ -88,7 +88,7 @@ case class BITCOIN_FUNDING_OTHER_CHANNEL_SPENT(channelId: Long) extends BitcoinE
|
||||
*/
|
||||
|
||||
sealed trait Command
|
||||
final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = BinaryData("00" * 1254), origin: Origin = Local, commit: Boolean = false) extends Command
|
||||
final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, expiry: Long, onion: BinaryData = BinaryData("00" * 1254), upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false) extends Command
|
||||
final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = false) extends Command
|
||||
final case class CMD_FAIL_HTLC(id: Long, reason: String, commit: Boolean = false) extends Command
|
||||
case object CMD_SIGN extends Command
|
||||
|
@ -107,6 +107,10 @@ object Commitments extends Logging {
|
||||
}
|
||||
|
||||
def receiveAdd(commitments: Commitments, add: UpdateAddHtlc): Commitments = {
|
||||
isOldAdd(commitments, add) match {
|
||||
case true => commitments
|
||||
case false =>
|
||||
|
||||
if (add.id != commitments.remoteNextHtlcId) {
|
||||
throw new RuntimeException(s"unexpected htlc id: actual=${add.id} expected=${commitments.remoteNextHtlcId}")
|
||||
}
|
||||
@ -145,76 +149,65 @@ object Commitments extends Logging {
|
||||
|
||||
commitments1
|
||||
}
|
||||
}
|
||||
|
||||
def isHtlcCrossSigned(commitments: Commitments, cmd: CMD_FULFILL_HTLC): Boolean = {
|
||||
val remoteSigned = commitments.localCommit.spec.htlcs.exists(htlc => htlc.direction == IN && htlc.add.id == cmd.id)
|
||||
def getHtlcCrossSigned(commitments: Commitments, directionRelativeToLocal: Direction, htlcId: Long): Option[UpdateAddHtlc] = {
|
||||
val remoteSigned = commitments.localCommit.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal && htlc.add.id == htlcId)
|
||||
val localSigned = commitments.remoteNextCommitInfo match {
|
||||
case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && htlc.add.id == cmd.id)
|
||||
case Right(_) => commitments.remoteCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && htlc.add.id == cmd.id)
|
||||
case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal.opposite && htlc.add.id == htlcId)
|
||||
case Right(_) => commitments.remoteCommit.spec.htlcs.find(htlc => htlc.direction == directionRelativeToLocal.opposite && htlc.add.id == htlcId)
|
||||
}
|
||||
if (!remoteSigned || !localSigned) {
|
||||
logger.warn(
|
||||
s"""htlc ${cmd.id} remoteSigned=$remoteSigned localSigned=$localSigned
|
||||
|${specsToString(commitments)}""".stripMargin)
|
||||
}
|
||||
remoteSigned && localSigned
|
||||
for {
|
||||
htlc_out <- remoteSigned
|
||||
htlc_in <- localSigned
|
||||
} yield htlc_in.add
|
||||
}
|
||||
|
||||
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) = {
|
||||
require(isHtlcCrossSigned(commitments, cmd), s"unknown htlc id=${cmd.id}")
|
||||
commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == cmd.id => u.add } match {
|
||||
def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): (Commitments, UpdateFulfillHtlc) =
|
||||
getHtlcCrossSigned(commitments, IN, cmd.id) match {
|
||||
case Some(htlc) if htlc.paymentHash == sha256(cmd.r) =>
|
||||
val fulfill = UpdateFulfillHtlc(commitments.channelId, cmd.id, cmd.r)
|
||||
val commitments1 = addLocalProposal(commitments, fulfill)
|
||||
(commitments1, fulfill)
|
||||
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc id=${cmd.id}")
|
||||
case None => ??? // never happens
|
||||
}
|
||||
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
|
||||
}
|
||||
|
||||
def isOldFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Boolean = {
|
||||
def isOldFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Boolean =
|
||||
commitments.remoteChanges.proposed.contains(fulfill) ||
|
||||
commitments.remoteChanges.signed.contains(fulfill) ||
|
||||
commitments.remoteChanges.acked.contains(fulfill)
|
||||
}
|
||||
|
||||
def isHtlcCrossSigned(commitments: Commitments, fulfill: UpdateFulfillHtlc): Boolean = {
|
||||
val remoteSigned = commitments.localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && htlc.add.id == fulfill.id)
|
||||
val localSigned = commitments.remoteNextCommitInfo match {
|
||||
case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && htlc.add.id == fulfill.id)
|
||||
case Right(_) => commitments.remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && htlc.add.id == fulfill.id)
|
||||
}
|
||||
if (!remoteSigned || !localSigned) {
|
||||
logger.warn(
|
||||
s"""htlc ${fulfill.id} remoteSigned=$remoteSigned localSigned=$localSigned
|
||||
|${specsToString(commitments)}""".stripMargin)
|
||||
}
|
||||
remoteSigned && localSigned
|
||||
}
|
||||
|
||||
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): (Commitments, UpdateAddHtlc) = {
|
||||
require(isHtlcCrossSigned(commitments, fulfill), s"unknown htlc id=${fulfill.id}")
|
||||
commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.direction == IN && u.add.id == fulfill.id => u.add } match {
|
||||
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => (addRemoteProposal(commitments, fulfill), htlc)
|
||||
def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Either[Commitments, Commitments] =
|
||||
isOldFulfill(commitments, fulfill) match {
|
||||
case true => Left(commitments)
|
||||
case false => getHtlcCrossSigned(commitments, OUT, fulfill.id) match {
|
||||
case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Right(addRemoteProposal(commitments, fulfill))
|
||||
case Some(htlc) => throw new RuntimeException(s"invalid htlc preimage for htlc id=${fulfill.id}")
|
||||
case None => ??? // never happens
|
||||
case None => throw new RuntimeException(s"unknown htlc id=${fulfill.id}")
|
||||
}
|
||||
}
|
||||
|
||||
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC): (Commitments, UpdateFailHtlc) = {
|
||||
commitments.localCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == cmd.id => u.add } match {
|
||||
def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC): (Commitments, UpdateFailHtlc) =
|
||||
getHtlcCrossSigned(commitments, IN, cmd.id) match {
|
||||
case Some(htlc) =>
|
||||
val fail = UpdateFailHtlc(commitments.channelId, cmd.id, BinaryData(cmd.reason.getBytes("UTF-8")))
|
||||
val commitments1 = addLocalProposal(commitments, fail)
|
||||
(commitments1, fail)
|
||||
case None => throw new RuntimeException(s"unknown htlc id=${cmd.id}")
|
||||
}
|
||||
}
|
||||
|
||||
def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): (Commitments, UpdateAddHtlc) = {
|
||||
commitments.remoteCommit.spec.htlcs.collectFirst { case u: Htlc if u.add.id == fail.id => u.add } match {
|
||||
case Some(htlc) => (addRemoteProposal(commitments, fail), htlc)
|
||||
case None => throw new RuntimeException(s"unknown htlc id=${fail.id}") // TODO: we should fail the channel
|
||||
def isOldFail(commitments: Commitments, fail: UpdateFailHtlc): Boolean =
|
||||
commitments.remoteChanges.proposed.contains(fail) ||
|
||||
commitments.remoteChanges.signed.contains(fail) ||
|
||||
commitments.remoteChanges.acked.contains(fail)
|
||||
|
||||
def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Either[Commitments, Commitments] =
|
||||
isOldFail(commitments, fail) match {
|
||||
case true => Left(commitments)
|
||||
case false => getHtlcCrossSigned(commitments, OUT, fail.id) match {
|
||||
case Some(htlc) => Right(addRemoteProposal(commitments, fail))
|
||||
case None => throw new RuntimeException(s"unknown htlc id=${fail.id}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,11 +255,12 @@ object Commitments extends Logging {
|
||||
}
|
||||
}
|
||||
|
||||
def isOldCommit(commitments: Commitments, commit: CommitSig): Boolean = {
|
||||
commitments.localCommit.commit == commit
|
||||
}
|
||||
def isOldCommit(commitments: Commitments, commit: CommitSig): Boolean = commitments.localCommit.commit == commit
|
||||
|
||||
def receiveCommit(commitments: Commitments, commit: CommitSig): (Commitments, RevokeAndAck) = {
|
||||
def receiveCommit(commitments: Commitments, commit: CommitSig): Either[Commitments, (Commitments, RevokeAndAck)] =
|
||||
isOldCommit(commitments, commit) match {
|
||||
case true => Left(commitments)
|
||||
case false =>
|
||||
import commitments._
|
||||
// they sent us a signature for *their* view of *our* next commit tx
|
||||
// so in terms of rev.hashes and indexes we have:
|
||||
@ -346,18 +340,20 @@ object Commitments extends Logging {
|
||||
|
||||
logger.debug(s"current commit: index=${ourCommit1.index} htlc_in=${ourCommit1.spec.htlcs.filter(_.direction == IN).size} htlc_out=${ourCommit1.spec.htlcs.filter(_.direction == OUT).size} txid=${ourCommit1.publishableTxs.commitTx.tx.txid} tx=${Transaction.write(ourCommit1.publishableTxs.commitTx.tx)}")
|
||||
|
||||
(commitments1, revocation)
|
||||
Right((commitments1, revocation))
|
||||
}
|
||||
|
||||
def isOldRevocation(commitments: Commitments, revocation: RevokeAndAck): Boolean = {
|
||||
def isOldRevocation(commitments: Commitments, revocation: RevokeAndAck): Boolean =
|
||||
commitments.remoteNextCommitInfo match {
|
||||
case Right(point) if point == revocation.nextPerCommitmentPoint => true
|
||||
case Left(waitForRevocation) if waitForRevocation.nextRemoteCommit.remotePerCommitmentPoint == revocation.nextPerCommitmentPoint => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): Commitments = {
|
||||
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): Either[Commitments, Commitments] =
|
||||
isOldRevocation(commitments, revocation) match {
|
||||
case true => Left(commitments)
|
||||
case false =>
|
||||
import commitments._
|
||||
// we receive a revocation because we just sent them a sig for their next commit tx
|
||||
remoteNextCommitInfo match {
|
||||
@ -368,7 +364,11 @@ object Commitments extends Logging {
|
||||
val (_, htlcTimeoutTxs, _) = makeRemoteTxs(theirNextCommit.index, localParams, remoteParams, commitInput, theirNextCommit.remotePerCommitmentPoint, theirNextCommit.spec)
|
||||
// then we sort and sign them
|
||||
val sortedHtlcTimeoutTxs = htlcTimeoutTxs.sortBy(_.input.outPoint.index)
|
||||
require(revocation.htlcTimeoutSignatures.size == sortedHtlcTimeoutTxs.size, s"htlc-timeout sig count mismatch (received=${revocation.htlcTimeoutSignatures.size}, expected=${sortedHtlcTimeoutTxs.size})")
|
||||
require(revocation.htlcTimeoutSignatures.size == sortedHtlcTimeoutTxs.size, s"htlc-timeout sig count mismatch (received=${
|
||||
revocation.htlcTimeoutSignatures.size
|
||||
}, expected=${
|
||||
sortedHtlcTimeoutTxs.size
|
||||
})")
|
||||
val paymentKey = Generators.derivePrivKey(localParams.paymentKey, theirNextCommit.remotePerCommitmentPoint)
|
||||
val htlcSigs = sortedHtlcTimeoutTxs.map(Transactions.sign(_, paymentKey))
|
||||
// combine the sigs to make signed txes
|
||||
@ -385,13 +385,15 @@ object Commitments extends Logging {
|
||||
// so we can acknowledge all our previous updates and the commitsig
|
||||
val unackedMessages1 = commitments.unackedMessages.drop(commitments.unackedMessages.indexWhere(_.isInstanceOf[CommitSig]) + 1)
|
||||
|
||||
commitments.copy(
|
||||
val commitments1 = commitments.copy(
|
||||
localChanges = localChanges.copy(signed = Nil, acked = localChanges.acked ++ localChanges.signed),
|
||||
remoteChanges = remoteChanges.copy(signed = Nil),
|
||||
remoteCommit = theirNextCommit,
|
||||
remoteNextCommitInfo = Right(revocation.nextPerCommitmentPoint),
|
||||
unackedMessages = unackedMessages1,
|
||||
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index))
|
||||
|
||||
Right(commitments1)
|
||||
case Right(_) =>
|
||||
throw new RuntimeException("received unexpected RevokeAndAck message")
|
||||
}
|
||||
@ -428,7 +430,7 @@ object Commitments extends Logging {
|
||||
case _ => "???"
|
||||
}
|
||||
|
||||
def toString(commitments: Commitments): String = {
|
||||
def changesToString(commitments: Commitments): String = {
|
||||
import commitments._
|
||||
s"""commitments:
|
||||
| localChanges:
|
||||
|
@ -8,8 +8,8 @@ import fr.acinq.bitcoin.BinaryData
|
||||
*/
|
||||
class PaymentEvent
|
||||
|
||||
case class PaymentSent(channel: ActorRef, h: BinaryData) extends PaymentEvent
|
||||
//case class PaymentSent(channel: ActorRef, h: BinaryData) extends PaymentEvent
|
||||
|
||||
case class PaymentFailed(channel: ActorRef, h: BinaryData, reason: String) extends PaymentEvent
|
||||
//case class PaymentFailed(channel: ActorRef, h: BinaryData, reason: String) extends PaymentEvent
|
||||
|
||||
case class PaymentReceived(channel: ActorRef, h: BinaryData) extends PaymentEvent
|
||||
|
@ -8,7 +8,7 @@ import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire.{Codecs, PerHopPayload}
|
||||
import fr.acinq.eclair.wire.{Codecs, PerHopPayload, UpdateFailHtlc, UpdateFulfillHtlc}
|
||||
import scodec.Attempt
|
||||
|
||||
// @formatter:off
|
||||
@ -46,7 +46,6 @@ class PaymentLifecycle(sourceNodeId: BinaryData, router: ActorRef) extends Loggi
|
||||
case Event(RouteResponse(hops), WaitingForRoute(s, c)) =>
|
||||
val firstHop = hops.head
|
||||
val cmd = buildCommand(c.amountMsat, c.paymentHash, hops, Globals.blockCount.get().toInt)
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
|
||||
context.actorSelection(Register.actorPathToChannelId(firstHop.lastUpdate.channelId)) ! cmd
|
||||
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, cmd)
|
||||
|
||||
@ -58,15 +57,17 @@ class PaymentLifecycle(sourceNodeId: BinaryData, router: ActorRef) extends Loggi
|
||||
when(WAITING_FOR_PAYMENT_COMPLETE) {
|
||||
case Event("ok", _) => stay()
|
||||
|
||||
case Event(e@PaymentSent(_, h), WaitingForComplete(s, cmd)) if h == cmd.paymentHash =>
|
||||
s ! "sent"
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(reason: String, WaitingForComplete(s, _)) =>
|
||||
s ! Status.Failure(new RuntimeException(reason))
|
||||
stop(FSM.Failure(reason))
|
||||
|
||||
case Event(e@PaymentFailed(_, h, reason), WaitingForComplete(s, cmd)) if h == cmd.paymentHash =>
|
||||
case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, cmd)) =>
|
||||
s ! "sent"
|
||||
stop(FSM.Normal)
|
||||
|
||||
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, cmd)) =>
|
||||
// TODO: fix new String(fail.reason)
|
||||
val reason = new String(fail.reason)
|
||||
s ! Status.Failure(new RuntimeException(reason))
|
||||
stop(FSM.Failure(reason))
|
||||
}
|
||||
@ -128,7 +129,7 @@ object PaymentLifecycle {
|
||||
val nodes = hops.map(_.nextNodeId)
|
||||
// BOLT 2 requires that associatedData == paymentHash
|
||||
val onion = buildOnion(nodes, payloads, paymentHash)
|
||||
CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion, commit = true)
|
||||
CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion, upstream_opt = None, commit = true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,10 +17,13 @@ import scala.util.{Failure, Success, Try}
|
||||
case class OutgoingChannel(channelId: Long, channel: ActorRef, nodeAddress: BinaryData)
|
||||
|
||||
sealed trait Origin
|
||||
case object Local extends Origin
|
||||
case class Relayed(downstream: UpdateAddHtlc) extends Origin
|
||||
case class Local(sender: ActorRef) extends Origin
|
||||
case class Relayed(upstream: UpdateAddHtlc) extends Origin
|
||||
|
||||
case class Binding(add: UpdateAddHtlc, origin: Origin)
|
||||
case class Bind(add: UpdateAddHtlc, origin: Origin)
|
||||
case class ForwardAdd(add: UpdateAddHtlc)
|
||||
case class ForwardFulfill(fulfill: UpdateFulfillHtlc)
|
||||
case class ForwardFail(fail: UpdateFailHtlc)
|
||||
|
||||
// @formatter:on
|
||||
|
||||
@ -34,26 +37,28 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
|
||||
|
||||
override def receive: Receive = main(Set(), Map())
|
||||
|
||||
def main(upstreams: Set[OutgoingChannel], bindings: Map[UpdateAddHtlc, Origin]): Receive = {
|
||||
case class DownstreamHtlcId(channelId: Long, htlcId: Long)
|
||||
|
||||
def main(channels: Set[OutgoingChannel], bindings: Map[DownstreamHtlcId, Origin]): Receive = {
|
||||
|
||||
case ChannelChangedState(channel, _, remoteNodeId, _, NORMAL, d: DATA_NORMAL) =>
|
||||
import d.commitments.channelId
|
||||
log.info(s"adding channel $channelId to available upstreams")
|
||||
context become main(upstreams + OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
log.info(s"adding channel $channelId to available channels")
|
||||
context become main(channels + OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
|
||||
case ChannelChangedState(channel, _, remoteNodeId, _, NEGOTIATING, d: DATA_NEGOTIATING) =>
|
||||
import d.commitments.channelId
|
||||
log.info(s"removing channel $channelId from upstreams/downstreams (mutual close)")
|
||||
log.info(s"removing channel $channelId from available channels")
|
||||
// TODO: cleanup bindings
|
||||
context become main(upstreams - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
context become main(channels - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
|
||||
case ChannelChangedState(channel, _, remoteNodeId, _, CLOSING, d: DATA_CLOSING) =>
|
||||
import d.commitments.channelId
|
||||
log.info(s"removing channel $channelId from upstreams/downstreams (unilateral close)")
|
||||
log.info(s"removing channel $channelId from available channels")
|
||||
// TODO: cleanup bindings
|
||||
context become main(upstreams - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
context become main(channels - OutgoingChannel(channelId, channel, remoteNodeId.hash160), bindings)
|
||||
|
||||
case add: UpdateAddHtlc =>
|
||||
case ForwardAdd(add) =>
|
||||
Try(Sphinx.parsePacket(nodeSecret, add.paymentHash, add.onionRoutingPacket))
|
||||
.map {
|
||||
case (payload, nextNodeAddress, nextPacket) => (Codecs.perHopPayloadCodec.decode(BitVector(payload.data)), nextNodeAddress, nextPacket)
|
||||
@ -62,13 +67,13 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
|
||||
log.info(s"we are the final recipient of htlc #${add.id}")
|
||||
context.system.eventStream.publish(PaymentReceived(self, add.paymentHash))
|
||||
paymentHandler forward add
|
||||
case Success((Attempt.Successful(DecodeResult(payload, _)), nextNodeAddress, nextPacket)) if upstreams.exists(_.nodeAddress == nextNodeAddress) =>
|
||||
val upstream = upstreams.find(_.nodeAddress == nextNodeAddress).get.channel
|
||||
log.info(s"forwarding htlc #${add.id} to upstream=$upstream")
|
||||
upstream ! CMD_ADD_HTLC(payload.amt_to_forward, add.paymentHash, payload.outgoing_cltv_value, nextPacket, origin = Relayed(add), commit = true)
|
||||
context become main(upstreams, bindings)
|
||||
case Success((Attempt.Successful(DecodeResult(payload, _)), nextNodeAddress, nextPacket)) if channels.exists(_.nodeAddress == nextNodeAddress) =>
|
||||
val downstream = channels.find(_.nodeAddress == nextNodeAddress).get.channel
|
||||
log.info(s"forwarding htlc #${add.id} to downstream=$downstream")
|
||||
downstream ! CMD_ADD_HTLC(payload.amt_to_forward, add.paymentHash, payload.outgoing_cltv_value, nextPacket, upstream_opt = Some(add), commit = true)
|
||||
context become main(channels, bindings)
|
||||
case Success((Attempt.Successful(DecodeResult(_, _)), nextNodeAddress, _)) =>
|
||||
log.warning(s"couldn't resolve upstream node address $nextNodeAddress, failing htlc #${add.id}")
|
||||
log.warning(s"couldn't resolve downstream node address $nextNodeAddress, failing htlc #${add.id}")
|
||||
sender ! CMD_FAIL_HTLC(add.id, "route error", commit = true)
|
||||
case Success((Attempt.Failure(cause), _, _)) =>
|
||||
log.error(s"couldn't parse payload: $cause")
|
||||
@ -78,45 +83,41 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
|
||||
sender ! CMD_FAIL_HTLC(add.id, "onion parsing error", commit = true)
|
||||
}
|
||||
|
||||
case Binding(upstream, origin) =>
|
||||
case Bind(downstream, origin) =>
|
||||
origin match {
|
||||
case Local => log.info(s"we are the origin of htlc ${upstream.channelId}/${upstream.id}")
|
||||
case Relayed(downstream) => log.info(s"relayed htlc ${downstream.channelId}/${downstream.id} to ${upstream.channelId}/${upstream.id}")
|
||||
case Local(_) => log.info(s"we are the origin of htlc ${downstream.channelId}/${downstream.id}")
|
||||
case Relayed(upstream) => log.info(s"relayed htlc ${upstream.channelId}/${upstream.id} to ${downstream}/${downstream.id}")
|
||||
}
|
||||
context become main(upstreams, bindings + (upstream -> origin))
|
||||
context become main(channels, bindings + (DownstreamHtlcId(downstream.channelId, downstream.id) -> origin))
|
||||
|
||||
case (add: UpdateAddHtlc, fulfill: UpdateFulfillHtlc) =>
|
||||
bindings.get(add) match {
|
||||
case Some(Relayed(origin)) if upstreams.exists(_.channelId == origin.channelId) =>
|
||||
val downstream = upstreams.find(_.channelId == origin.channelId).get.channel
|
||||
downstream ! CMD_SIGN
|
||||
downstream ! CMD_FULFILL_HTLC(origin.id, fulfill.paymentPreimage)
|
||||
downstream ! CMD_SIGN
|
||||
case ForwardFulfill(fulfill) =>
|
||||
bindings.get(DownstreamHtlcId(fulfill.channelId, fulfill.id)) match {
|
||||
case Some(Relayed(origin)) if channels.exists(_.channelId == origin.channelId) =>
|
||||
val upstream = channels.find(_.channelId == origin.channelId).get.channel
|
||||
upstream ! CMD_FULFILL_HTLC(origin.id, fulfill.paymentPreimage, commit = true)
|
||||
case Some(Relayed(origin)) =>
|
||||
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
|
||||
case Some(Local) =>
|
||||
case Some(Local(sender)) =>
|
||||
log.info(s"we were the origin payer for htlc #${fulfill.id}")
|
||||
context.system.eventStream.publish(PaymentSent(self, add.paymentHash))
|
||||
sender ! fulfill
|
||||
case None =>
|
||||
log.warning(s"no origin found for htlc $add")
|
||||
log.warning(s"no origin found for htlc ${fulfill.channelId}/${fulfill.id}")
|
||||
}
|
||||
|
||||
case (add: UpdateAddHtlc, fail: UpdateFailHtlc) =>
|
||||
bindings.get(add) match {
|
||||
case Some(Relayed(origin)) if upstreams.exists(_.channelId == origin.channelId) =>
|
||||
val downstream = upstreams.find(_.channelId == origin.channelId).get.channel
|
||||
downstream ! CMD_SIGN
|
||||
case ForwardFail(fail) =>
|
||||
bindings.get(DownstreamHtlcId(fail.channelId, fail.id)) match {
|
||||
case Some(Relayed(origin)) if channels.exists(_.channelId == origin.channelId) =>
|
||||
val upstream = channels.find(_.channelId == origin.channelId).get.channel
|
||||
// TODO: fix new String(fail.reason)
|
||||
downstream ! CMD_FAIL_HTLC(origin.id, new String(fail.reason))
|
||||
downstream ! CMD_SIGN
|
||||
upstream ! CMD_FAIL_HTLC(origin.id, new String(fail.reason), commit = true)
|
||||
upstream ! CMD_SIGN
|
||||
case Some(Relayed(origin)) =>
|
||||
log.warning(s"origin channel ${origin.channelId} has disappeared in the meantime")
|
||||
case Some(Local) =>
|
||||
case Some(Local(sender)) =>
|
||||
log.info(s"we were the origin payer for htlc #${fail.id}")
|
||||
// TODO: fix new String(fail.reason)
|
||||
context.system.eventStream.publish(PaymentFailed(self, add.paymentHash, new String(fail.reason)))
|
||||
sender ! fail
|
||||
case None =>
|
||||
log.warning(s"no origin found for htlc $add")
|
||||
log.warning(s"no origin found for htlc ${fail.channelId}/${fail.id}")
|
||||
}
|
||||
|
||||
case w@WatchEventSpent(BITCOIN_HTLC_SPENT, tx) =>
|
||||
@ -139,13 +140,13 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
|
||||
.flatten
|
||||
.map { preimage =>
|
||||
bindings.collect {
|
||||
case b@(upstream, Relayed(downstream)) if downstream.paymentHash == sha256(preimage) =>
|
||||
case b@(downstreamHtlcId, Relayed(upstream)) if upstream.paymentHash == sha256(preimage) =>
|
||||
log.info(s"found a match between preimage=$preimage and origin htlc for $b")
|
||||
self ! (upstream, UpdateFulfillHtlc(upstream.channelId, upstream.id, preimage))
|
||||
self ! ForwardFulfill(UpdateFulfillHtlc(downstreamHtlcId.channelId, downstreamHtlcId.htlcId, preimage))
|
||||
}
|
||||
}
|
||||
|
||||
case 'upstreams => sender ! upstreams
|
||||
case 'channels => sender ! channels
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,9 @@ import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc, U
|
||||
*/
|
||||
|
||||
// @formatter:off
|
||||
sealed trait Direction
|
||||
case object IN extends Direction
|
||||
case object OUT extends Direction
|
||||
sealed trait Direction { def opposite: Direction }
|
||||
case object IN extends Direction { def opposite = OUT }
|
||||
case object OUT extends Direction { def opposite = IN }
|
||||
// @formatter:on
|
||||
|
||||
case class Htlc(direction: Direction, add: UpdateAddHtlc, val previousChannelId: Option[BinaryData])
|
||||
|
@ -8,7 +8,7 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="CONSOLEWARN" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!--appender name="CONSOLEWARN" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.out</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>WARN</level>
|
||||
@ -16,7 +16,7 @@
|
||||
<encoder>
|
||||
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</appender-->
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>eclair.log</file>
|
||||
@ -26,17 +26,13 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="fr.acinq.eclair.Pipe" level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<logger name="fr.acinq.eclair.channel.states.NormalOfflineFuzzySpec" level="INFO">
|
||||
<logger name="fr.acinq.eclair.Pipe" level="DEBUG">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<root level="INFO">
|
||||
<!--appender-ref ref="FILE"/-->
|
||||
<!--appender-ref ref="CONSOLEWARN"/-->
|
||||
<!--appender-ref ref="FILE"/>
|
||||
<appender-ref ref="CONSOLEWARN"/-->
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
|
@ -0,0 +1,128 @@
|
||||
package fr.acinq.eclair.channel.states
|
||||
|
||||
import akka.actor.{ActorRef, Cancellable, Props}
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.collection.immutable.Nil
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 05/07/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
|
||||
type FixtureParam = Tuple7[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], ActorRef, ActorRef, ActorRef, ActorRef, ActorRef]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val pipe = system.actorOf(Props(new Pipe()))
|
||||
val alice2blockchain = TestProbe()
|
||||
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient())))
|
||||
val bob2blockchain = TestProbe()
|
||||
val paymentHandlerA = system.actorOf(Props(new LocalPaymentHandler()), name = "payment-handler-a")
|
||||
val paymentHandlerB = system.actorOf(Props(new LocalPaymentHandler()), name = "payment-handler-b")
|
||||
val relayerA = system.actorOf(Relayer.props(Globals.Node.privateKey, paymentHandlerA), "relayer-a")
|
||||
val relayerB = system.actorOf(Relayer.props(Globals.Node.privateKey, paymentHandlerB), "relayer-b")
|
||||
val router = TestProbe()
|
||||
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, alice2blockchain.ref, router.ref, relayerA))
|
||||
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, bob2blockchain.ref, router.ref, relayerB))
|
||||
within(30 seconds) {
|
||||
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
|
||||
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
|
||||
relayerA ! alice
|
||||
relayerB ! bob
|
||||
alice ! INPUT_INIT_FUNDER(Bob.id, 0, TestConstants.fundingSatoshis, TestConstants.pushMsat, Alice.channelParams, bobInit)
|
||||
bob ! INPUT_INIT_FUNDEE(Alice.id, 0, Bob.channelParams, aliceInit)
|
||||
pipe ! (alice, bob)
|
||||
alice2blockchain.expectMsgType[MakeFundingTx]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
alice2blockchain.expectMsgType[WatchConfirmed]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
alice2blockchain.expectMsgType[PublishAsap]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
bob2blockchain.expectMsgType[WatchSpent]
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
|
||||
alice2blockchain.expectMsgType[WatchLost]
|
||||
bob2blockchain.expectMsgType[WatchLost]
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
}
|
||||
test((alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))
|
||||
}
|
||||
|
||||
def buildCmdAdd(paymentHash: BinaryData) = {
|
||||
val channelUpdate_ab = ChannelUpdate("00" * 64, 0, 0, "0000", cltvExpiryDelta = 4, feeBaseMsat = 642000, feeProportionalMillionths = 7, htlcMinimumMsat = 0)
|
||||
val hops = Hop(Globals.Node.publicKey, Globals.Node.publicKey, channelUpdate_ab) :: Nil
|
||||
// we don't want to be below htlcMinimumMsat
|
||||
val amount = Random.nextInt(1000000) + 1000
|
||||
PaymentLifecycle.buildCommand(amount, paymentHash, hops, 444000)
|
||||
}
|
||||
|
||||
def gatling(parallel: Int, total: Int, channel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef): Unit = {
|
||||
for (i <- 0 until total / parallel) {
|
||||
// we don't want to be above maxHtlcValueInFlightMsat or maxAcceptedHtlcs
|
||||
awaitCond(channel.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.htlcs.size < 10 && channel.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteCommit.spec.htlcs.size < 10)
|
||||
val senders = for (i <- 0 until parallel) yield TestProbe()
|
||||
senders.foreach(_.send(paymentHandler, 'genh))
|
||||
val paymentHashes = senders.map(_.expectMsgType[BinaryData])
|
||||
val cmds = paymentHashes.map(buildCmdAdd(_))
|
||||
senders.zip(cmds).foreach(x => x._1.send(channel, x._2))
|
||||
val oks = senders.map(_.expectMsgType[String])
|
||||
val fulfills = senders.map(_.expectMsgType[UpdateFulfillHtlc])
|
||||
}
|
||||
}
|
||||
|
||||
def randomDisconnect(initialPipe: ActorRef): Cancellable = {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
var currentPipe = initialPipe
|
||||
system.scheduler.schedule(3 seconds, 3 seconds) {
|
||||
currentPipe ! INPUT_DISCONNECTED
|
||||
val newPipe = system.actorOf(Props(new Pipe()))
|
||||
system.scheduler.scheduleOnce(500 millis) {
|
||||
currentPipe ! INPUT_RECONNECTED(newPipe)
|
||||
currentPipe = newPipe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("fuzzy testing with only one party sending HTLCs") {
|
||||
case (alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB) =>
|
||||
val gatling1 = new Thread(new Runnable {
|
||||
override def run(): Unit = gatling(5, 100, alice, paymentHandlerB)
|
||||
})
|
||||
gatling1.start()
|
||||
val chaosMonkey = randomDisconnect(pipe)
|
||||
gatling1.join()
|
||||
chaosMonkey.cancel()
|
||||
}
|
||||
|
||||
test("fuzzy testing with only both parties sending HTLCs") {
|
||||
case (alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB) =>
|
||||
val gatling1 = new Thread(new Runnable {
|
||||
override def run(): Unit = gatling(4, 100, alice, paymentHandlerB)
|
||||
})
|
||||
gatling1.start()
|
||||
val gatling2 = new Thread(new Runnable {
|
||||
override def run(): Unit = gatling(4, 100, bob, paymentHandlerA)
|
||||
})
|
||||
gatling2.start()
|
||||
val chaosMonkey = randomDisconnect(pipe)
|
||||
gatling1.join()
|
||||
gatling2.join()
|
||||
chaosMonkey.cancel()
|
||||
}
|
||||
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package fr.acinq.eclair.channel.states
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto}
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Pipe, TestBitcoinClient, TestConstants, TestkitBaseClass}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* Created by PM on 05/07/2016.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class NormalOfflineFuzzySpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
|
||||
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestFSMRef[State, Data, Channel], ActorRef, ActorRef, ActorRef]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
val pipe = system.actorOf(Props(new Pipe()))
|
||||
val alice2blockchain = TestProbe()
|
||||
val blockchainA = system.actorOf(Props(new PeerWatcher(new TestBitcoinClient())))
|
||||
val bob2blockchain = TestProbe()
|
||||
val relayerA = system.actorOf(Props(new FuzzyRelayer(410000)))
|
||||
val relayerB = system.actorOf(Props(new FuzzyRelayer(420000)))
|
||||
val router = TestProbe()
|
||||
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, alice2blockchain.ref, router.ref, relayerA))
|
||||
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(pipe, bob2blockchain.ref, router.ref, relayerB))
|
||||
within(30 seconds) {
|
||||
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
|
||||
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
|
||||
relayerA ! alice
|
||||
relayerB ! bob
|
||||
alice ! INPUT_INIT_FUNDER(Bob.id, 0, TestConstants.fundingSatoshis, TestConstants.pushMsat, Alice.channelParams, bobInit)
|
||||
bob ! INPUT_INIT_FUNDEE(Alice.id, 0, Bob.channelParams, aliceInit)
|
||||
pipe ! (alice, bob)
|
||||
alice2blockchain.expectMsgType[MakeFundingTx]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
alice2blockchain.expectMsgType[WatchSpent]
|
||||
alice2blockchain.expectMsgType[WatchConfirmed]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
alice2blockchain.expectMsgType[PublishAsap]
|
||||
alice2blockchain.forward(blockchainA)
|
||||
bob2blockchain.expectMsgType[WatchSpent]
|
||||
bob2blockchain.expectMsgType[WatchConfirmed]
|
||||
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
|
||||
alice2blockchain.expectMsgType[WatchLost]
|
||||
bob2blockchain.expectMsgType[WatchLost]
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
}
|
||||
test((alice, bob, pipe, relayerA, relayerB))
|
||||
}
|
||||
|
||||
class FuzzyRelayer(expiry: Int) extends Actor with ActorLogging {
|
||||
|
||||
val paymentpreimage = BinaryData("42" * 32)
|
||||
val paymentHash = Crypto.sha256(paymentpreimage)
|
||||
|
||||
override def receive: Receive = {
|
||||
case channel: ActorRef => context become ready(channel)
|
||||
}
|
||||
|
||||
def ready(channel: ActorRef): Receive = {
|
||||
case 'start => context become main(sender, channel, 0, 0)
|
||||
}
|
||||
|
||||
def main(origin: ActorRef, channel: ActorRef, htlcSent: Int, htlcInFlight: Int): Receive = {
|
||||
|
||||
case htlc: UpdateAddHtlc =>
|
||||
val preimage = BinaryData("42" * 32)
|
||||
sender ! CMD_FULFILL_HTLC(htlc.id, preimage)
|
||||
sender ! CMD_SIGN
|
||||
|
||||
case (add: UpdateAddHtlc, fulfill: UpdateFulfillHtlc) =>
|
||||
if (htlcInFlight <= 1) {
|
||||
if (htlcSent < 100) {
|
||||
log.info(s"already sent $htlcSent, inFlight=${htlcInFlight - 1}")
|
||||
self ! 'add
|
||||
} else {
|
||||
origin ! "done"
|
||||
}
|
||||
}
|
||||
context become main(origin, channel, htlcSent, htlcInFlight - 1)
|
||||
|
||||
case 'add =>
|
||||
val cmds = for (i <- 0 to Random.nextInt(10)) yield CMD_ADD_HTLC(Random.nextInt(1000000) + 1000, paymentHash, 400144)
|
||||
//val cmds = CMD_ADD_HTLC(Random.nextInt(1000000), paymentHash, expiry) :: Nil
|
||||
cmds.foreach(channel ! _)
|
||||
channel ! CMD_SIGN
|
||||
context become main(origin, channel, htlcSent + cmds.size, htlcInFlight + cmds.size)
|
||||
|
||||
case "ok" => {}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test("fuzzy testing with only one party sending HTLCs") {
|
||||
case (alice, bob, pipe, relayerA, relayerB) =>
|
||||
val sender = TestProbe()
|
||||
sender.send(relayerA, 'start)
|
||||
sender.send(relayerB, 'start)
|
||||
relayerA ! 'add
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
var currentPipe = pipe
|
||||
|
||||
val task = system.scheduler.schedule(3 seconds, 3 seconds) {
|
||||
currentPipe ! INPUT_DISCONNECTED
|
||||
val newPipe = system.actorOf(Props(new Pipe()))
|
||||
system.scheduler.scheduleOnce(500 millis) {
|
||||
currentPipe ! INPUT_RECONNECTED(newPipe)
|
||||
currentPipe = newPipe
|
||||
}
|
||||
}
|
||||
sender.expectMsg(10 minutes, "done")
|
||||
task.cancel()
|
||||
}
|
||||
|
||||
test("fuzzy testing in with both parties sending HTLCs") {
|
||||
case (alice, bob, pipe, relayerA, relayerB) =>
|
||||
val sender = TestProbe()
|
||||
sender.send(relayerA, 'start)
|
||||
sender.send(relayerB, 'start)
|
||||
relayerA ! 'add
|
||||
relayerB ! 'add
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
var currentPipe = pipe
|
||||
|
||||
val task = system.scheduler.schedule(3 seconds, 3 seconds) {
|
||||
currentPipe ! INPUT_DISCONNECTED
|
||||
val newPipe = system.actorOf(Props(new Pipe()))
|
||||
system.scheduler.scheduleOnce(500 millis) {
|
||||
currentPipe ! INPUT_RECONNECTED(newPipe)
|
||||
currentPipe = newPipe
|
||||
}
|
||||
}
|
||||
sender.expectMsg(10 minutes, "done")
|
||||
sender.expectMsg(10 minutes, "done")
|
||||
task.cancel()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.payment.{Binding, Local, Relayed}
|
||||
import fr.acinq.eclair.payment.{Bind, Local, Relayed}
|
||||
import fr.acinq.eclair.transactions.{IN, OUT}
|
||||
import fr.acinq.eclair.wire.{ClosingSigned, CommitSig, Error, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
|
||||
@ -59,7 +59,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil),
|
||||
unackedMessages = htlc :: Nil
|
||||
)))
|
||||
relayer.expectMsg(Binding(htlc, origin = Local))
|
||||
relayer.expectMsg(Bind(htlc, origin = Local(sender.ref)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val sender = TestProbe()
|
||||
val h = BinaryData("00112233445566778899aabbccddeeff")
|
||||
val originHtlc = UpdateAddHtlc(channelId = 4298564, id = 5656, amountMsat = 50000000, expiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254)
|
||||
val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, origin = Relayed(originHtlc))
|
||||
val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.expiry - 7, upstream_opt = Some(originHtlc))
|
||||
sender.send(alice, cmd)
|
||||
sender.expectMsg("ok")
|
||||
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
@ -93,7 +93,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
localNextHtlcId = 1,
|
||||
localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil),
|
||||
unackedMessages = htlc :: Nil)))
|
||||
relayer.expectMsg(Binding(htlc, origin = Relayed(originHtlc)))
|
||||
relayer.expectMsg(Bind(htlc, origin = Relayed(originHtlc)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -677,7 +677,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
|
||||
sender.send(bob, CMD_FULFILL_HTLC(42, r))
|
||||
sender.expectMsg("requirement failed: unknown htlc id=42")
|
||||
sender.expectMsg("unknown htlc id=42")
|
||||
assert(initialState == bob.stateData)
|
||||
}
|
||||
}
|
||||
|
@ -119,17 +119,16 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
// => A expects rev2 but it will first receive rev1
|
||||
val comm_a1_1 = comm_a
|
||||
// A ignores rev1
|
||||
assert(Commitments.isOldRevocation(comm_a1_1, ba_rev_1) === true)
|
||||
assert(Commitments.receiveRevocation(comm_a1_1, ba_rev_1).isLeft)
|
||||
// since A sent back sig2 so b replies with rev2
|
||||
val comm_a1_2 = Commitments.receiveRevocation(comm_a1_1, ba_rev_2)
|
||||
val comm_a1_2 = Commitments.receiveRevocation(comm_a1_1, ba_rev_2).right.get
|
||||
assert(comm_a1_2.unackedMessages.map(Commitments.msg2String(_)).mkString(" ") === "")
|
||||
|
||||
// SCENARIO A2: B did receive sig2
|
||||
// => A expects rev2 and will receive it
|
||||
val comm_a2_1 = comm_a
|
||||
// a will first receive sig2
|
||||
assert(Commitments.isOldRevocation(comm_a2_1, ba_rev_2) === false)
|
||||
val comm_a2_2 = Commitments.receiveRevocation(comm_a2_1, ba_rev_2)
|
||||
val comm_a2_2 = Commitments.receiveRevocation(comm_a2_1, ba_rev_2).right.get
|
||||
assert(comm_a2_2.unackedMessages.map(Commitments.msg2String(_)).mkString(" ") === "")
|
||||
|
||||
// SCENARIO B1: B did receive sig2
|
||||
|
@ -97,7 +97,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val sender = TestProbe()
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
|
||||
sender.send(bob, CMD_FULFILL_HTLC(42, "12" * 32))
|
||||
sender.expectMsg("requirement failed: unknown htlc id=42")
|
||||
sender.expectMsg("unknown htlc id=42")
|
||||
assert(initialState == bob.stateData)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import akka.actor.Status.Failure
|
||||
import akka.testkit.TestProbe
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.router.BaseRouterSpec
|
||||
import fr.acinq.eclair.wire.{UpdateFailHtlc, UpdateFulfillHtlc}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@ -46,7 +47,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
||||
sender.send(paymentFSM, PaymentFailed(null, request.paymentHash, "some reason"))
|
||||
sender.send(paymentFSM, UpdateFailHtlc(0, 0, "some reason".getBytes))
|
||||
|
||||
val res = sender.expectMsgType[Failure]
|
||||
assert(res.cause.getMessage === "some reason")
|
||||
@ -56,6 +57,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router))
|
||||
val monitor = TestProbe()
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
@ -65,10 +68,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
||||
sender.send(paymentFSM, PaymentSent(null, request.paymentHash))
|
||||
sender.send(paymentFSM, UpdateFulfillHtlc(0, 0, "42" * 32))
|
||||
|
||||
val res = sender.expectMsgType[String]
|
||||
assert(res === "sent")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,17 +24,15 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
// let's reuse the existing test data
|
||||
import HtlcGenerationSpec._
|
||||
|
||||
type FixtureParam = Tuple3[ActorRef, TestProbe, TestProbe]
|
||||
type FixtureParam = Tuple2[ActorRef, TestProbe]
|
||||
|
||||
override def withFixture(test: OneArgTest) = {
|
||||
|
||||
within(30 seconds) {
|
||||
val paymentHandler = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
// we are node B in the route A -> B -> C -> ....
|
||||
val relayer = system.actorOf(Relayer.props(priv_b, paymentHandler.ref))
|
||||
test((relayer, paymentHandler, eventListener))
|
||||
test((relayer, paymentHandler))
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,47 +42,49 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
val channelId_ab = 981408633
|
||||
val channelId_bc = 237534
|
||||
|
||||
test("add a channel") { case (relayer, _, _) =>
|
||||
test("add a channel") { case (relayer, _) =>
|
||||
val sender = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, 'upstreams)
|
||||
sender.send(relayer, 'channels)
|
||||
val upstreams = sender.expectMsgType[Set[OutgoingChannel]]
|
||||
assert(upstreams === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
|
||||
}
|
||||
|
||||
test("remove a channel (mutual close)") { case (relayer, _, eventListener) =>
|
||||
test("remove a channel (mutual close)") { case (relayer, _) =>
|
||||
val sender = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, 'upstreams)
|
||||
sender.send(relayer, 'channels)
|
||||
val upstreams1 = sender.expectMsgType[Set[OutgoingChannel]]
|
||||
assert(upstreams1 === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, SHUTDOWN, NEGOTIATING, DATA_NEGOTIATING(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null, null, null)))
|
||||
sender.send(relayer, 'upstreams)
|
||||
sender.send(relayer, 'channels)
|
||||
val upstreams2 = sender.expectMsgType[Set[OutgoingChannel]]
|
||||
assert(upstreams2 === Set.empty)
|
||||
}
|
||||
|
||||
test("remove a channel (unilateral close)") { case (relayer, _, eventListener) =>
|
||||
test("remove a channel (unilateral close)") { case (relayer, _) =>
|
||||
val sender = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, 'upstreams)
|
||||
sender.send(relayer, 'channels)
|
||||
val upstreams1 = sender.expectMsgType[Set[OutgoingChannel]]
|
||||
assert(upstreams1 === Set(OutgoingChannel(channelId_bc, channel_bc.ref, nodeId_c.hash160)))
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, NORMAL, CLOSING, DATA_CLOSING(Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), None, Some(null), None, None, Nil)))
|
||||
sender.send(relayer, 'upstreams)
|
||||
sender.send(relayer, 'channels)
|
||||
val upstreams2 = sender.expectMsgType[Set[OutgoingChannel]]
|
||||
assert(upstreams2 === Set.empty)
|
||||
}
|
||||
|
||||
test("send an event when we receive a payment") { case (relayer, paymentHandler, eventListener) =>
|
||||
test("send an event when we receive a payment") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
|
||||
val add_ab = {
|
||||
val cmd = buildCommand(finalAmountMsat, paymentHash, hops.take(1), currentBlockCount)
|
||||
@ -92,7 +92,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
|
||||
}
|
||||
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val add1 = paymentHandler.expectMsgType[UpdateAddHtlc]
|
||||
eventListener.expectMsgType[PaymentReceived]
|
||||
@ -101,7 +101,8 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
}
|
||||
|
||||
test("relay an htlc-add") { case (relayer, paymentHandler, eventListener) =>
|
||||
|
||||
test("relay an htlc-add") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
@ -112,17 +113,17 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
}
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
sender.expectNoMsg(1 second)
|
||||
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
|
||||
paymentHandler.expectNoMsg(1 second)
|
||||
|
||||
assert(cmd_bc.origin === Relayed(add_ab))
|
||||
assert(cmd_bc.upstream_opt === Some(add_ab))
|
||||
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, paymentHandler, _) =>
|
||||
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
@ -132,7 +133,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
|
||||
}
|
||||
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
channel_bc.expectNoMsg(1 second)
|
||||
@ -142,7 +143,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, paymentHandler, _) =>
|
||||
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, paymentHandler) =>
|
||||
|
||||
// TODO: we should use the new update_fail_malformed_htlc message (see BOLT 2)
|
||||
val sender = TestProbe()
|
||||
@ -154,7 +155,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, "00" * 1254)
|
||||
}
|
||||
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
channel_bc.expectNoMsg(1 second)
|
||||
@ -164,7 +165,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
}
|
||||
|
||||
test("relay an htlc-fulfill") { case (relayer, paymentHandler, eventListener) =>
|
||||
test("relay an htlc-fulfill") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
@ -177,46 +178,21 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_ab), null)))
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
|
||||
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
|
||||
sender.send(relayer, Binding(add_bc, Relayed(add_ab)))
|
||||
sender.send(relayer, Bind(add_bc, Relayed(add_ab)))
|
||||
// preimage is wrong, does not matter here
|
||||
val fulfill_cb = UpdateFulfillHtlc(channelId = add_bc.channelId, id = add_bc.id, paymentPreimage = "00" * 32)
|
||||
sender.send(relayer, (add_bc, fulfill_cb))
|
||||
sender.send(relayer, ForwardFulfill(fulfill_cb))
|
||||
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
val fulfill_ba = channel_ab.expectMsgType[CMD_FULFILL_HTLC]
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
eventListener.expectNoMsg(1 second)
|
||||
|
||||
assert(fulfill_ba.id === add_ab.id)
|
||||
|
||||
}
|
||||
|
||||
test("send an event when we receive an htlc-fulfill and we were the initiator") { case (relayer, paymentHandler, eventListener) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
// note we simulate this by not having a binding for this channel
|
||||
|
||||
val add_ab = {
|
||||
val cmd = buildCommand(finalAmountMsat, paymentHash, hops.take(1), currentBlockCount)
|
||||
// and then manually build an htlc
|
||||
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
|
||||
}
|
||||
sender.send(relayer, Binding(add_ab, Local))
|
||||
// preimage is wrong, does not matter here
|
||||
val fulfill_cb = UpdateFulfillHtlc(channelId = add_ab.channelId, id = add_ab.id, paymentPreimage = "00" * 32)
|
||||
sender.send(relayer, (add_ab, fulfill_cb))
|
||||
|
||||
channel_ab.expectNoMsg(1 second)
|
||||
eventListener.expectMsgType[PaymentSent]
|
||||
|
||||
}
|
||||
|
||||
test("relay an htlc-fail") { case (relayer, paymentHandler, eventListener) =>
|
||||
test("relay an htlc-fail") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
@ -229,44 +205,20 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_ab), null)))
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
|
||||
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
|
||||
sender.send(relayer, Binding(add_bc, Relayed(add_ab)))
|
||||
sender.send(relayer, Bind(add_bc, Relayed(add_ab)))
|
||||
val fail_cb = UpdateFailHtlc(channelId = add_bc.channelId, id = add_bc.id, reason = "some reason".getBytes())
|
||||
sender.send(relayer, (add_bc, fail_cb))
|
||||
sender.send(relayer, ForwardFail(fail_cb))
|
||||
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
val fulfill_ba = channel_ab.expectMsgType[CMD_FAIL_HTLC]
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
eventListener.expectNoMsg(1 second)
|
||||
|
||||
assert(fulfill_ba.id === add_ab.id)
|
||||
|
||||
}
|
||||
|
||||
test("send an event when we receive an htlc-fail and we were the initiator") { case (relayer, paymentHandler, eventListener) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
|
||||
// note we simulate this by not having a binding for this channel
|
||||
|
||||
val add_ab = {
|
||||
val cmd = buildCommand(finalAmountMsat, paymentHash, hops, currentBlockCount)
|
||||
// and then manually build an htlc
|
||||
UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.expiry, cmd.paymentHash, cmd.onion)
|
||||
}
|
||||
sender.send(relayer, Binding(add_ab, Local))
|
||||
val fail_cb = UpdateFailHtlc(channelId = add_ab.channelId, id = add_ab.id, reason = "some reason".getBytes())
|
||||
sender.send(relayer, (add_ab, fail_cb))
|
||||
|
||||
channel_ab.expectNoMsg(1 second)
|
||||
eventListener.expectMsgType[PaymentFailed]
|
||||
|
||||
}
|
||||
|
||||
test("extract a payment preimage from an onchain tx (extract from witnessHtlcSuccess script)") { case (relayer, paymentHandler, eventListener) =>
|
||||
test("extract a payment preimage from an onchain tx (extract from witnessHtlcSuccess script)") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
@ -279,23 +231,21 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_ab), null)))
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
|
||||
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
|
||||
sender.send(relayer, Binding(add_bc, Relayed(add_ab)))
|
||||
sender.send(relayer, Bind(add_bc, Relayed(add_ab)))
|
||||
|
||||
// actual test starts here
|
||||
val tx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessHtlcSuccess("11" * 70, "22" * 70, paymentPreimage, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)
|
||||
sender.send(relayer, WatchEventSpent(BITCOIN_HTLC_SPENT, tx))
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
val cmd_ab = channel_ab.expectMsgType[CMD_FULFILL_HTLC]
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
|
||||
assert(cmd_ab.id === add_ab.id)
|
||||
|
||||
}
|
||||
|
||||
test("extract a payment preimage from an onchain tx (extract from witnessClaimHtlcSuccessFromCommitTx script)") { case (relayer, paymentHandler, eventListener) =>
|
||||
test("extract a payment preimage from an onchain tx (extract from witnessClaimHtlcSuccessFromCommitTx script)") { case (relayer, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
val channel_ab = TestProbe()
|
||||
val channel_bc = TestProbe()
|
||||
@ -308,17 +258,15 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
sender.send(relayer, ChannelChangedState(channel_ab.ref, null, nodeId_a, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_ab), null)))
|
||||
sender.send(relayer, ChannelChangedState(channel_bc.ref, null, nodeId_c, WAIT_FOR_FUNDING_LOCKED, NORMAL, DATA_NORMAL(null, Commitments(null, null, null, null, null, null, 0, 0, null, null, null, null, channelId_bc), null)))
|
||||
sender.send(relayer, add_ab)
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
val cmd_bc = channel_bc.expectMsgType[CMD_ADD_HTLC]
|
||||
val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 987451, amountMsat = cmd_bc.amountMsat, expiry = cmd_bc.expiry, paymentHash = cmd_bc.paymentHash, onionRoutingPacket = cmd_bc.onion)
|
||||
sender.send(relayer, Binding(add_bc, Relayed(add_ab)))
|
||||
sender.send(relayer, Bind(add_bc, Relayed(add_ab)))
|
||||
|
||||
// actual test starts here
|
||||
val tx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx("11" * 70, paymentPreimage, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)
|
||||
sender.send(relayer, WatchEventSpent(BITCOIN_HTLC_SPENT, tx))
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
val cmd_ab = channel_ab.expectMsgType[CMD_FULFILL_HTLC]
|
||||
channel_ab.expectMsg(CMD_SIGN)
|
||||
|
||||
assert(cmd_ab.id === add_ab.id)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user