1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 22:25:26 +01:00

Relay fail message *after* it is cross-signed (#754)

We were previously handling `UpdateFailHtlc` and
`UpdateFailMalformedHtlc` similarly to `UpdateFulfillHtlc`, but that is
wrong:
- a fulfill needs to be propagated as soon as possible, because it
allows us to pull funds from upstream
- a fail needs to be cross-signed downstream (=irrevocably confirmed)
before forwarding it upstream, because it means that we won't
be able to pull funds anymore. In other words we need to be absolutely
sure that the htlc won't be fulfilled downstream if we fail it upstream,
otherwise we risk losing money.

Also added tests.
This commit is contained in:
Pierre-Marie Padiou 2019-01-02 16:44:05 +01:00 committed by GitHub
parent 7f3b101426
commit 8887ac2043
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 310 additions and 172 deletions

View file

@ -546,7 +546,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fulfill: UpdateFulfillHtlc, d: DATA_NORMAL) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success(Right((commitments1, origin, htlc))) =>
// NB: fulfills must be forwarded to the upstream channel asap, because they allow us to get money
// we forward preimages as soon as possible to the upstream channel because it allows us to pull funds
relayer ! ForwardFulfill(fulfill, origin, htlc)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
@ -571,9 +571,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fail: UpdateFailHtlc, d: DATA_NORMAL) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success(Right((commitments1, origin, htlc))) =>
// TODO NB: fails must not be forwarded to the upstream channel before they are signed, because they cancel the incoming payment
relayer ! ForwardFail(fail, origin, htlc)
case Success(Right((commitments1, _, _))) =>
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d, Some(fail))
@ -581,9 +579,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fail: UpdateFailMalformedHtlc, d: DATA_NORMAL) =>
Try(Commitments.receiveFailMalformed(d.commitments, fail)) match {
case Success(Right((commitments1, origin, htlc))) =>
// TODO NB: fails must not be forwarded to the upstream channel before they are signed, because they cancel the incoming payment
relayer ! ForwardFailMalformed(fail, origin, htlc)
case Success(Right((commitments1, _, _))) =>
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d, Some(fail))
@ -657,16 +653,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// 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((commitments1, forwards)) =>
cancelTimer(RevocationTimeout.toString)
// 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 ! ForwardAdd(htlc)
}
log.debug(s"received a new rev, spec:\n${Commitments.specs2String(commitments1)}")
forwards.foreach { forward =>
log.debug(s"forwarding {} to relayer", forward)
relayer ! forward
}
if (Commitments.localHasChanges(commitments1) && d.commitments.remoteNextCommitInfo.left.map(_.reSignAsap) == Left(true)) {
self ! CMD_SIGN
}
@ -881,6 +874,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fulfill: UpdateFulfillHtlc, d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFulfill(d.commitments, fulfill)) match {
case Success(Right((commitments1, origin, htlc))) =>
// we forward preimages as soon as possible to the upstream channel because it allows us to pull funds
relayer ! ForwardFulfill(fulfill, origin, htlc)
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
@ -905,9 +899,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fail: UpdateFailHtlc, d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFail(d.commitments, fail)) match {
case Success(Right((commitments1, origin, htlc))) =>
// TODO NB: fails must not be forwarded to the upstream channel before they are signed, because they cancel the incoming payment
relayer ! ForwardFail(fail, origin, htlc)
case Success(Right((commitments1, _, _))) =>
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d, Some(fail))
@ -915,9 +907,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(fail: UpdateFailMalformedHtlc, d: DATA_SHUTDOWN) =>
Try(Commitments.receiveFailMalformed(d.commitments, fail)) match {
case Success(Right((commitments1, origin, htlc))) =>
// TODO NB: fails must not be forwarded to the upstream channel before they are signed, because they cancel the incoming payment
relayer ! ForwardFailMalformed(fail, origin, htlc)
case Success(Right((commitments1, _, _))) =>
stay using d.copy(commitments = commitments1)
case Success(Left(_)) => stay
case Failure(cause) => handleLocalError(cause, d, Some(fail))
@ -963,28 +953,27 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
}
case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) =>
Try(Commitments.receiveCommit(d.commitments, commit, keyManager)) map {
case (commitments1, revocation) =>
Try(Commitments.receiveCommit(d.commitments, commit, keyManager)) match {
case Success((commitments1, revocation)) =>
// we always reply with a revocation
log.debug(s"received a new sig:\n${Commitments.specs2String(commitments1)}")
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
(commitments1, revocation)
} match {
case Success((commitments1, revocation)) if commitments1.hasNoPendingHtlcs =>
if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil
if (commitments1.hasNoPendingHtlcs) {
if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending revocation :: closingSigned :: Nil
} else {
// we are fundee, will wait for their closing_signed
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None)) sending revocation
}
} else {
// we are fundee, will wait for their closing_signed
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None)) sending revocation
if (Commitments.localHasChanges(commitments1)) {
// if we have newly acknowledged changes let's sign them
self ! CMD_SIGN
}
stay using store(d.copy(commitments = commitments1)) sending revocation
}
case Success((commitments1, revocation)) =>
if (Commitments.localHasChanges(commitments1)) {
// if we have newly acknowledged changes let's sign them
self ! CMD_SIGN
}
stay using store(d.copy(commitments = commitments1)) sending revocation
case Failure(cause) => handleLocalError(cause, d, Some(commit))
}
@ -992,30 +981,34 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// we received a revocation because we sent a signature
// => all our changes have been acked including the shutdown message
Try(Commitments.receiveRevocation(commitments, revocation)) match {
case Success(commitments1) if commitments1.hasNoPendingHtlcs =>
case Success((commitments1, forwards)) =>
cancelTimer(RevocationTimeout.toString)
log.debug(s"received a new rev, switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}")
if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned
} else {
// we are fundee, will wait for their closing_signed
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None))
}
case Success(commitments1) =>
cancelTimer(RevocationTimeout.toString)
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
d.commitments.remoteChanges.signed.collect {
case htlc: UpdateAddHtlc =>
log.debug(s"closing in progress: failing $htlc")
self ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure), commit = true)
}
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.specs2String(commitments1)}")
stay using store(d.copy(commitments = commitments1))
forwards.foreach {
case forwardAdd: ForwardAdd =>
// BOLT 2: A sending node SHOULD fail to route any HTLC added after it sent shutdown.
log.debug(s"closing in progress: failing ${forwardAdd.add}")
self ! CMD_FAIL_HTLC(forwardAdd.add.id, Right(PermanentChannelFailure), commit = true)
case forward =>
log.debug(s"forwarding {} to relayer", forward)
relayer ! forward
}
if (commitments1.hasNoPendingHtlcs) {
log.debug(s"switching to NEGOTIATING spec:\n${Commitments.specs2String(commitments1)}")
if (d.commitments.localParams.isFunder) {
// we are funder, need to initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey)
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx.tx, closingSigned))), bestUnpublishedClosingTx_opt = None)) sending closingSigned
} else {
// we are fundee, will wait for their closing_signed
goto(NEGOTIATING) using store(DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None))
}
} else {
if (Commitments.localHasChanges(commitments1) && d.commitments.remoteNextCommitInfo.left.map(_.reSignAsap) == Left(true)) {
self ! CMD_SIGN
}
stay using store(d.copy(commitments = commitments1))
}
case Failure(cause) => handleLocalError(cause, d, Some(revocation))
}

View file

@ -20,7 +20,7 @@ import akka.event.LoggingAdapter
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, sha256}
import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi, Satoshi, Transaction}
import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx}
import fr.acinq.eclair.payment.Origin
import fr.acinq.eclair.payment._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
@ -455,30 +455,47 @@ object Commitments {
publishableTxs = PublishableTxs(signedCommitTx, htlcTxsAndSigs))
val ourChanges1 = localChanges.copy(acked = Nil)
val theirChanges1 = remoteChanges.copy(proposed = Nil, acked = remoteChanges.acked ++ remoteChanges.proposed)
// the outgoing following htlcs have been completed (fulfilled or failed) when we received this sig
val completedOutgoingHtlcs = commitments.localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add.id) -- localCommit1.spec.htlcs.filter(_.direction == OUT).map(_.add.id)
// we remove the newly completed htlcs from the origin map
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1, originChannels = originChannels1)
val commitments1 = commitments.copy(localCommit = localCommit1, localChanges = ourChanges1, remoteChanges = theirChanges1)
(commitments1, revocation)
}
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): Commitments = {
def receiveRevocation(commitments: Commitments, revocation: RevokeAndAck): (Commitments, Seq[ForwardMessage]) = {
import commitments._
// we receive a revocation because we just sent them a sig for their next commit tx
remoteNextCommitInfo match {
case Left(_) if revocation.perCommitmentSecret.toPoint != remoteCommit.remotePerCommitmentPoint =>
throw InvalidRevocation(commitments.channelId)
case Left(WaitingForRevocation(theirNextCommit, _, _, _)) =>
val forwards = commitments.remoteChanges.signed collect {
// we forward adds downstream only when they have been committed by both sides
// it always happen when we receive a revocation, because they send the add, then they sign it, then we sign it
case add: UpdateAddHtlc => ForwardAdd(add)
// same for fails: we need to make sure that they are in neither commitment before propagating the fail upstream
case fail: UpdateFailHtlc =>
val origin = commitments.originChannels(fail.id)
val add = commitments.remoteCommit.spec.htlcs.find(p => p.direction == IN && p.add.id == fail.id).map(_.add).get
ForwardFail(fail, origin, add)
// same as above
case fail: UpdateFailMalformedHtlc =>
val origin = commitments.originChannels(fail.id)
val add = commitments.remoteCommit.spec.htlcs.find(p => p.direction == IN && p.add.id == fail.id).map(_.add).get
ForwardFailMalformed(fail, origin, add)
}
// the outgoing following htlcs have been completed (fulfilled or failed) when we received this revocation
// they have been removed from both local and remote commitment
// (since fulfill/fail are sent by remote, they are (1) signed by them, (2) revoked by us, (3) signed by us, (4) revoked by them
val completedOutgoingHtlcs = commitments.remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id) -- theirNextCommit.spec.htlcs.filter(_.direction == IN).map(_.add.id)
// we remove the newly completed htlcs from the origin map
val originChannels1 = commitments.originChannels -- completedOutgoingHtlcs
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),
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index))
commitments1
remotePerCommitmentSecrets = commitments.remotePerCommitmentSecrets.addHash(revocation.perCommitmentSecret, 0xFFFFFFFFFFFFL - commitments.remoteCommit.index),
originChannels = originChannels1)
(commitments1, forwards)
case Right(_) =>
throw UnexpectedRevocation(commitments.channelId)
}

View file

@ -39,10 +39,11 @@ sealed trait Origin
case class Local(sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors
case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin
case class ForwardAdd(add: UpdateAddHtlc, canRedirect: Boolean = true)
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin, htlc: UpdateAddHtlc)
case class ForwardFail(fail: UpdateFailHtlc, to: Origin, htlc: UpdateAddHtlc)
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin, htlc: UpdateAddHtlc)
sealed trait ForwardMessage
case class ForwardAdd(add: UpdateAddHtlc, canRedirect: Boolean = true) extends ForwardMessage
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage
case class ForwardFail(fail: UpdateFailHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin, htlc: UpdateAddHtlc) extends ForwardMessage
// @formatter:on

View file

@ -16,6 +16,7 @@
package fr.acinq.eclair.channel.states
import akka.actor.Actor
import akka.testkit.{TestFSMRef, TestKitBase, TestProbe}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
@ -37,43 +38,38 @@ trait StateTestsHelperMethods extends TestKitBase {
def defaultOnion: BinaryData = "00" * Sphinx.PacketLength
case class Setup(alice: TestFSMRef[State, Data, Channel],
case class SetupFixture(alice: TestFSMRef[State, Data, Channel],
bob: TestFSMRef[State, Data, Channel],
alice2bob: TestProbe,
bob2alice: TestProbe,
alice2blockchain: TestProbe,
bob2blockchain: TestProbe,
router: TestProbe,
relayer: TestProbe)
relayerA: TestProbe,
relayerB: TestProbe,
channelUpdateListener: TestProbe)
def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams): Setup = {
def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams): SetupFixture = {
Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw))
val alice2bob = TestProbe()
val bob2alice = TestProbe()
val alice2blockchain = TestProbe()
val bob2blockchain = TestProbe()
val relayer = TestProbe()
system.eventStream.subscribe(relayer.ref, classOf[LocalChannelUpdate])
system.eventStream.subscribe(relayer.ref, classOf[LocalChannelDown])
// hacky... publish a fake update and wait till we've received it
// val fakeUpdate = LocalChannelUpdate(probe.ref, BinaryData("00" * 32), 0, PrivateKey("01" * 32).publicKey, None, ChannelUpdate(LightningMessageCodecsSpec.randomSignature, Block.RegtestGenesisBlock.hash, 1, 2, BinaryData("0202"), 3, 4, 5, 6))
// system.eventStream.publish(fakeUpdate)
// probe.expectMsg(fakeUpdate)
val relayerA = TestProbe()
val relayerB = TestProbe()
val channelUpdateListener = TestProbe()
system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate])
system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown])
val router = TestProbe()
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayer.ref))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayer.ref))
Setup(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayer)
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, router.ref, relayerA.ref))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, router.ref, relayerB.ref))
SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener)
}
def reachNormal(alice: TestFSMRef[State, Data, Channel],
bob: TestFSMRef[State, Data, Channel],
alice2bob: TestProbe,
bob2alice: TestProbe,
alice2blockchain: TestProbe,
bob2blockchain: TestProbe,
relayer: TestProbe,
def reachNormal(setup: SetupFixture,
tags: Set[String] = Set.empty): Unit = {
import setup._
val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams)
val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures)
@ -107,8 +103,8 @@ trait StateTestsHelperMethods extends TestKitBase {
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
// x2 because alice and bob share the same relayer
relayer.expectMsgType[LocalChannelUpdate]
relayer.expectMsgType[LocalChannelUpdate]
channelUpdateListener.expectMsgType[LocalChannelUpdate]
channelUpdateListener.expectMsgType[LocalChannelUpdate]
}
def addHtlc(amountMsat: Int, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): (BinaryData, UpdateAddHtlc) = {

View file

@ -18,13 +18,15 @@ package fr.acinq.eclair.channel.states.e
import akka.actor.Status
import akka.actor.Status.Failure
import akka.testkit.{TestFSMRef, TestProbe}
import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.UInt64.Conversions._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.Channel.TickRefreshChannelUpdate
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.Channel.{RevocationTimeout, TickRefreshChannelUpdate}
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
@ -45,16 +47,16 @@ import scala.concurrent.duration._
class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe)
type FixtureParam = SetupFixture
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, test.tags)
reachNormal(setup, test.tags)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)))
withFixture(test.toNoArgTest(setup))
}
}
@ -295,6 +297,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val htlc = UpdateAddHtlc("00" * 32, 0, 150000, BinaryData("42" * 32), 400144, defaultOnion)
bob ! htlc
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1)))
// bob won't forward the add before it is cross-signed
relayerB.expectNoMsg()
}
test("recv UpdateAddHtlc (unexpected id)") { f =>
@ -336,7 +340,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -351,7 +355,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -368,7 +372,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -384,7 +388,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -398,7 +402,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage)
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -416,7 +420,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -715,7 +719,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.expectMsgType[Error]
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -800,7 +804,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv RevokeAndAck (one htlc received)") { f =>
import f._
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
sender.send(alice, CMD_SIGN)
sender.expectMsg("ok")
@ -813,10 +817,17 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
// at this point bob still hasn't forwarded the htlc downstream
relayerB.expectNoMsg()
// actual test begins
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight)
// now bob will forward the htlc downstream
val forward = relayerB.expectMsgType[ForwardAdd]
assert(forward.add === htlc)
}
test("recv RevokeAndAck (multiple htlcs in both directions)") { f =>
@ -893,7 +904,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -908,12 +919,70 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
}
test("recv RevokeAndAck (forward UpdateFailHtlc)") { f =>
import f._
val sender = TestProbe()
val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)))
sender.expectMsg("ok")
val fail = bob2alice.expectMsgType[UpdateFailHtlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
// alice still hasn't forwarded the fail because it is not yet cross-signed
relayerA.expectNoMsg()
// actual test begins
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
// alice will forward the fail upstream
val forward = relayerA.expectMsgType[ForwardFail]
assert(forward.fail === fail)
assert(forward.htlc === htlc)
}
test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f =>
import f._
val sender = TestProbe()
val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION))
sender.expectMsg("ok")
val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
// alice still hasn't forwarded the fail because it is not yet cross-signed
relayerA.expectNoMsg()
// actual test begins
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
// alice will forward the fail upstream
val forward = relayerA.expectMsgType[ForwardFailMalformed]
assert(forward.fail === fail)
assert(forward.htlc === htlc)
}
test("recv RevocationTimeout") { f =>
import f._
val sender = TestProbe()
@ -985,6 +1054,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill))))
// alice immediately propagates the fulfill upstream
val forward = relayerA.expectMsgType[ForwardFulfill]
assert(forward.fulfill === fulfill)
assert(forward.htlc === htlc)
}
test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f =>
@ -1001,7 +1074,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -1015,7 +1088,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -1026,15 +1099,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
relayerB.expectMsgType[ForwardAdd]
val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
// actual test begins
sender.send(alice, UpdateFulfillHtlc("00" * 32, htlc.id, "00" * 32))
relayer.expectMsgType[ForwardAdd]
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap] // main delayed
alice2blockchain.expectMsgType[PublishAsap] // htlc timeout
@ -1106,9 +1179,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv UpdateFailHtlc") { f =>
import f._
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)))
sender.expectMsg("ok")
val fail = bob2alice.expectMsgType[UpdateFailHtlc]
@ -1118,6 +1190,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.forward(alice)
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))))
// alice won't forward the fail before it is cross-signed
relayerA.expectNoMsg()
}
test("recv UpdateFailMalformedHtlc") { f =>
@ -1125,9 +1199,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val sender = TestProbe()
// Alice sends an HTLC to Bob, which they both sign
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
// Bob fails the HTLC because he cannot parse it
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION))
@ -1137,6 +1210,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
awaitCond(alice.stateData == initialState.copy(
commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail))))
// alice won't forward the fail before it is cross-signed
relayerA.expectNoMsg()
sender.send(bob, CMD_SIGN)
val sig = bob2alice.expectMsgType[CommitSig]
@ -1182,7 +1257,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -1196,7 +1271,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -1264,7 +1339,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Error]
awaitCond(alice.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId)
alice2blockchain.expectMsg(PublishAsap(tx))
alice2blockchain.expectMsgType[PublishAsap]
alice2blockchain.expectMsgType[WatchConfirmed]
@ -1282,7 +1357,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === CannotAffordFees(channelId(bob), missingSatoshis = 71620000L, reserveSatoshis = 20000L, feesSatoshis = 72400000L).getMessage)
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx)) // commit tx
//bob2blockchain.expectMsgType[PublishAsap] // main delayed (removed because of the high fees)
bob2blockchain.expectMsgType[WatchConfirmed]
@ -1297,7 +1372,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === "local/remote feerates are too different: remoteFeeratePerKw=85000 localFeeratePerKw=10000")
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -1312,7 +1387,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(new String(error.data) === "remote fee rate is too small: remoteFeeratePerKw=252")
awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
bob2blockchain.expectMsg(PublishAsap(tx))
bob2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchConfirmed]
@ -1326,10 +1401,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
sender.send(alice, CMD_UPDATE_RELAY_FEE(newFeeBaseMsat, newFeeProportionalMillionth))
sender.expectMsg("ok")
val localUpdate = relayer.expectMsgType[LocalChannelUpdate]
val localUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate]
assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat)
assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth)
relayer.expectNoMsg(1 seconds)
relayerA.expectNoMsg(1 seconds)
}
test("recv CMD_CLOSE (no pending htlcs)") { f =>
@ -1405,7 +1480,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[ClosingSigned]
awaitCond(alice.stateName == NEGOTIATING)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId)
}
test("recv Shutdown (with unacked sent htlcs)") { f =>
@ -1426,7 +1501,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectMsgType[Shutdown]
awaitCond(alice.stateName == SHUTDOWN)
// channel should be advertised as down
assert(relayer.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId)
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId)
}
test("recv Shutdown (with unacked received htlcs)") { f =>
@ -1910,7 +1985,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2bob.expectNoMsg(1 second)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true)
// we don't re-publish the same channel_update if there was no change
relayer.expectNoMsg(1 second)
channelUpdateListener.expectNoMsg(1 second)
}
test("recv BITCOIN_FUNDING_DEEPLYBURIED (short channel id changed)", Tag("channels_public")) { f =>
@ -1921,8 +1996,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// public channel: we don't send the channel_update directly to the peer
alice2bob.expectNoMsg(1 second)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true)
assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId)
relayer.expectNoMsg(1 second)
assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId)
channelUpdateListener.expectNoMsg(1 second)
}
test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel)") { f =>
@ -1933,7 +2008,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val channelUpdate = alice2bob.expectMsgType[ChannelUpdate]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true)
// we don't re-publish the same channel_update if there was no change
relayer.expectNoMsg(1 second)
channelUpdateListener.expectNoMsg(1 second)
}
test("recv BITCOIN_FUNDING_DEEPLYBURIED (private channel, short channel id changed)") { f =>
@ -1944,8 +2019,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val channelUpdate = alice2bob.expectMsgType[ChannelUpdate]
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried == true)
// LocalChannelUpdate should not be published
assert(relayer.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId)
relayer.expectNoMsg(1 second)
assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId)
channelUpdateListener.expectNoMsg(1 second)
}
test("recv AnnouncementSignatures", Tag("channels_public")) { f =>
@ -1964,7 +2039,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
val normal = alice.stateData.asInstanceOf[DATA_NORMAL]
normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement == Some(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId
})
assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn))
assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn))
}
test("recv AnnouncementSignatures (re-send)", Tag("channels_public")) { f =>
@ -1994,12 +2069,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42))
bob2alice.expectMsgType[AnnouncementSignatures]
bob2alice.forward(alice)
val update1 = relayer.expectMsgType[LocalChannelUpdate]
val update1 = channelUpdateListener.expectMsgType[LocalChannelUpdate]
// actual test starts here
Thread.sleep(1100)
sender.send(alice, TickRefreshChannelUpdate)
val update2 = relayer.expectMsgType[LocalChannelUpdate]
val update2 = channelUpdateListener.expectMsgType[LocalChannelUpdate]
assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp)
}
@ -2010,13 +2085,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42))
bob2alice.expectMsgType[AnnouncementSignatures]
bob2alice.forward(alice)
val update1 = relayer.expectMsgType[LocalChannelUpdate]
val update1 = channelUpdateListener.expectMsgType[LocalChannelUpdate]
assert(Announcements.isEnabled(update1.channelUpdate.channelFlags) == true)
// actual test starts here
Thread.sleep(1100)
sender.send(alice, INPUT_DISCONNECTED)
val update2 = relayer.expectMsgType[LocalChannelUpdate]
val update2 = channelUpdateListener.expectMsgType[LocalChannelUpdate]
assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp)
assert(Announcements.isEnabled(update2.channelUpdate.channelFlags) == false)
awaitCond(alice.stateName == OFFLINE)
@ -2032,8 +2107,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// actual test starts here
sender.send(alice, INPUT_DISCONNECTED)
assert(relayer.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc1.paymentHash)
assert(relayer.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc2.paymentHash)
assert(relayerA.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc1.paymentHash)
assert(relayerA.expectMsgType[Status.Failure].cause.asInstanceOf[AddHtlcFailed].paymentHash === htlc2.paymentHash)
awaitCond(alice.stateName == OFFLINE)
}

View file

@ -36,16 +36,16 @@ import scala.concurrent.duration._
class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe)
type FixtureParam = SetupFixture
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)
reachNormal(setup)
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)))
withFixture(test.toNoArgTest(setup))
}
}
@ -337,15 +337,15 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
awaitCond(bob.stateName == OFFLINE)
// alice and bob announce that their channel is OFFLINE
assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false)
assert(Announcements.isEnabled(relayer.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false)
assert(Announcements.isEnabled(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false)
assert(Announcements.isEnabled(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags) == false)
// we make alice update here relay fee
sender.send(alice, CMD_UPDATE_RELAY_FEE(4200, 123456))
sender.expectMsg("ok")
// alice doesn't broadcast the new channel_update yet
relayer.expectNoMsg(300 millis)
channelUpdateListener.expectNoMsg(300 millis)
// then we reconnect them
sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit))
@ -358,13 +358,13 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.forward(alice)
// then alice reaches NORMAL state, and during the transition she broadcasts the channel_update
val channelUpdate = relayer.expectMsgType[LocalChannelUpdate](10 seconds).channelUpdate
val channelUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate](10 seconds).channelUpdate
assert(channelUpdate.feeBaseMsat === 4200)
assert(channelUpdate.feeProportionalMillionths === 123456)
assert(Announcements.isEnabled(channelUpdate.channelFlags) == true)
// no more messages
relayer.expectNoMsg(300 millis)
channelUpdateListener.expectNoMsg(300 millis)
}
}

View file

@ -24,7 +24,7 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment.{ForwardAdd, Local, PaymentLifecycle}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Hop
import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
@ -38,13 +38,13 @@ import scala.concurrent.duration._
class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe)
type FixtureParam = SetupFixture
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)
reachNormal(setup)
val sender = TestProbe()
// alice sends an HTLC to bob
val r1: BinaryData = "11" * 32
@ -80,8 +80,8 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.forward(alice)
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
relayer.expectMsgType[ForwardAdd]
relayer.expectMsgType[ForwardAdd]
relayerB.expectMsgType[ForwardAdd]
relayerB.expectMsgType[ForwardAdd]
// alice initiates a closing
sender.send(alice, CMD_CLOSE(None))
alice2bob.expectMsgType[Shutdown]
@ -90,7 +90,9 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
bob2alice.forward(alice)
awaitCond(alice.stateName == SHUTDOWN)
awaitCond(bob.stateName == SHUTDOWN)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)))
channelUpdateListener.expectMsgType[LocalChannelDown]
channelUpdateListener.expectMsgType[LocalChannelDown]
withFixture(test.toNoArgTest(setup))
}
}
@ -441,6 +443,58 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
alice2blockchain.expectMsgType[WatchConfirmed]
}
test("recv RevokeAndAck (forward UpdateFailHtlc)") { f =>
import f._
val sender = TestProbe()
sender.send(bob, CMD_FAIL_HTLC(1, Right(PermanentChannelFailure)))
sender.expectMsg("ok")
val fail = bob2alice.expectMsgType[UpdateFailHtlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
// alice still hasn't forwarded the fail because it is not yet cross-signed
relayerA.expectNoMsg()
// actual test begins
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
// alice will forward the fail upstream
val forward = relayerA.expectMsgType[ForwardFail]
assert(forward.fail === fail)
}
test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f =>
import f._
val sender = TestProbe()
sender.send(bob, CMD_FAIL_MALFORMED_HTLC(1, Crypto.sha256("should be htlc.onionRoutingPacket".getBytes()), FailureMessageCodecs.BADONION))
sender.expectMsg("ok")
val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc]
bob2alice.forward(alice)
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[RevokeAndAck]
alice2bob.forward(bob)
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
// alice still hasn't forwarded the fail because it is not yet cross-signed
relayerA.expectNoMsg()
// actual test begins
bob2alice.expectMsgType[RevokeAndAck]
bob2alice.forward(alice)
// alice will forward the fail upstream
val forward = relayerA.expectMsgType[ForwardFailMalformed]
assert(forward.fail === fail)
}
test("recv CMD_UPDATE_FEE") { f =>
import f._
val sender = TestProbe()

View file

@ -40,13 +40,13 @@ import scala.util.Success
class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe)
type FixtureParam = SetupFixture
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)
reachNormal(setup)
val sender = TestProbe()
// alice initiates a closing
if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4319)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(10000))
@ -61,7 +61,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods
if (test.tags.contains("fee2")) Globals.feeratesPerKw.set(FeeratesPerKw.single(4316)) else Globals.feeratesPerKw.set(FeeratesPerKw.single(5000))
alice2bob.forward(bob)
awaitCond(bob.stateName == NEGOTIATING)
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)))
withFixture(test.toNoArgTest(setup))
}
}

View file

@ -38,23 +38,25 @@ import scala.concurrent.duration._
class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayer: TestProbe, bobCommitTxes: List[PublishableTxs])
case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, bobCommitTxes: List[PublishableTxs])
override def withFixture(test: OneArgTest): Outcome = {
val setup = init()
import setup._
within(30 seconds) {
reachNormal(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer)
reachNormal(setup)
val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000, 200000000, 300000000)) yield {
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
relayer.expectMsgType[ForwardAdd]
relayerB.expectMsgType[ForwardAdd]
val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob)
relayer.expectMsgType[ForwardFulfill]
// alice forwards the fulfill upstream
relayerA.expectMsgType[ForwardFulfill]
crossSign(bob, alice, bob2alice, alice2bob)
relayer.expectMsgType[CommandBuffer.CommandAck]
// bob confirms that it has forwarded the fulfill to alice
relayerB.expectMsgType[CommandBuffer.CommandAck]
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
bobCommitTx1 :: bobCommitTx2 :: Nil
}).flatten
@ -70,7 +72,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// - revoked commit
// and we want to be able to test the different scenarii.
// Hence the NORMAL->CLOSING transition will occur in the individual tests.
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayer, bobCommitTxes)))
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, bobCommitTxes)))
}
}
@ -191,7 +193,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// alice sends an htlc to bob
val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
relayer.expectMsgType[ForwardAdd]
relayerB.expectMsgType[ForwardAdd]
// an error occurs and alice publishes her commit tx
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
alice ! Error("00" * 32, "oops".getBytes)
@ -208,17 +210,17 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(initialState.localCommitPublished.isDefined)
// actual test starts here
relayer.expectMsgType[LocalChannelDown]
channelUpdateListener.expectMsgType[LocalChannelDown]
// scenario 1: bob claims the htlc output from the commit tx using its preimage
val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx("11" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessFromCommitTx)
assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1))
assert(relayerA.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1))
// scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx)
val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint("22" * 32, 0), signatureScript = "", sequence = 0, witness = Scripts.witnessHtlcSuccess("11" * 70, "22" * 70, ra1, "33" * 130)) :: Nil, txOut = Nil, lockTime = 0)
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, claimHtlcSuccessTx)
assert(relayer.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1))
assert(relayerA.expectMsgType[ForwardFulfill].fulfill === UpdateFulfillHtlc(htlca1.channelId, htlca1.id, ra1))
assert(alice.stateData == initialState) // this was a no-op
}
@ -271,14 +273,14 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
awaitCond(alice.stateName == CLOSING)
val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(aliceData.localCommitPublished.isDefined)
relayer.expectMsgType[LocalChannelDown]
channelUpdateListener.expectMsgType[LocalChannelDown]
// actual test starts here
// when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 0, 0)
// so she fails it
val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)
relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
relayerA.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
}
test("recv BITCOIN_TX_CONFIRMED (remote commit with htlcs only signed by local in next remote commit)") { f =>
@ -296,14 +298,14 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
awaitCond(alice.stateName == CLOSING)
val aliceData = alice.stateData.asInstanceOf[DATA_CLOSING]
assert(aliceData.remoteCommitPublished.isDefined)
relayer.expectMsgType[LocalChannelDown]
channelUpdateListener.expectMsgType[LocalChannelDown]
// actual test starts here
// when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0)
// so she fails it
val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)
relayer.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
relayerA.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
}
test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f =>