diff --git a/README.md b/README.md index e0e2e026a..76793a79d 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ java -Declair.datadir=/tmp/node1 -jar eclair-node-gui--.jar checkpayment | paymentRequest | returns true if the payment has been received, false otherwise close | channelId | close a channel close | channelId, scriptPubKey | close a channel and send the funds to the given scriptPubKey + forceclose | channelId | force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)" help | | display available methods ## Docker diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 147411131..c18fd8deb 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-SNAPSHOT + 0.2-android-alpha13 eclair-core_2.11 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 08e0ddd50..9e2cab591 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -175,6 +175,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) goto(OFFLINE) using data } + + case Event(CMD_CLOSE(_), _) => goto(CLOSED) replying "ok" }) when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { @@ -221,7 +223,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, accept) sending accept } - case Event(CMD_CLOSE(_), _) => goto(CLOSED) + case Event(CMD_CLOSE(_), _) => goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) @@ -262,7 +264,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(CMD_CLOSE(_), _) => replyToUser(Right("closed")) - goto(CLOSED) + goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => replyToUser(Left(Right(e))) @@ -301,7 +303,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(CMD_CLOSE(_), _) => replyToUser(Right("closed")) - goto(CLOSED) + goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => replyToUser(Left(Right(e))) @@ -353,7 +355,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu goto(WAIT_FOR_FUNDING_CONFIRMED) using store(DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, Right(fundingSigned))) sending fundingSigned } - case Event(CMD_CLOSE(_), _) => goto(CLOSED) + case Event(CMD_CLOSE(_), _) => goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) @@ -404,11 +406,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu goto(WAIT_FOR_FUNDING_CONFIRMED) using nextState } - case Event(CMD_CLOSE(_), d: DATA_WAIT_FOR_FUNDING_SIGNED) => + case Event(CMD_CLOSE(_) | CMD_FORCECLOSE, d: DATA_WAIT_FOR_FUNDING_SIGNED) => // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) replyToUser(Right("closed")) - goto(CLOSED) + goto(CLOSED) replying "ok" case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => // we rollback the funding tx, it will never be published @@ -457,8 +459,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) - case Event(CMD_CLOSE(_), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => spendLocalCurrent(d) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) @@ -481,8 +481,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) - case Event(CMD_CLOSE(_), d: DATA_WAIT_FOR_FUNDING_LOCKED) => spendLocalCurrent(d) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) }) @@ -1077,6 +1075,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu if (d.mutualCloseProposed.map(_.txid).contains(tx.txid)) { // at any time they can publish a closing tx with any sig we sent them handleMutualClose(tx, Right(d)) + } else if (d.mutualClosePublished.map(_.txid).contains(tx.txid)) { + // we have published a closing tx which isn't one that we proposed, and used it instead of our last commitment when an error happened + handleMutualClose(tx, Right(d)) } else if (Some(tx.txid) == d.localCommitPublished.map(_.commitTx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay @@ -1224,8 +1225,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu goto(SYNCING) sending channelReestablish - case Event(c: CMD_CLOSE, d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while disconnected"), d, Some(c)) replying "ok" - case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) => // note: this can only happen if state is NORMAL or SHUTDOWN // -> in NEGOTIATING there are no more htlcs @@ -1334,8 +1333,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) sending d.localShutdown } - case Event(c: CMD_CLOSE, d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "can't do a mutual close while syncing"), d, Some(c)) - case Event(c@CurrentBlockCount(count), d: HasCommitments) if d.commitments.hasTimedoutOutgoingHtlcs(count) => handleLocalError(HtlcTimedout(d.channelId), d, Some(c)) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.map(_.unsignedTx.txid).contains(tx.txid) => handleMutualClose(tx, Left(d)) @@ -1367,8 +1364,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu whenUnhandled { - case Event(c@INPUT_PUBLISH_LOCALCOMMIT, d: HasCommitments) => handleLocalError(ForcedLocalCommit(d.channelId, "manual unilateral close"), d, Some(c)) - case Event(INPUT_DISCONNECTED, _) => goto(OFFLINE) case Event(WatchEventLost(BITCOIN_FUNDING_LOST), _) => goto(ERR_FUNDING_LOST) @@ -1394,6 +1389,14 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case _ => handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), None), c) // we don't provide a channel_update: this will be a permanent channel failure } + case Event(c: CMD_CLOSE, d) => handleCommandError(CannotCloseInThisState(Helpers.getChannelId(d), stateName), c) + + case Event(c@CMD_FORCECLOSE, d) => + d match { + case data: HasCommitments => handleLocalError(ForcedLocalCommit(data.channelId, "forced local commit"), data, Some(c)) replying "ok" + case _ => handleCommandError(CannotCloseInThisState(Helpers.getChannelId(d), stateName), c) + } + // we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream case Event(CurrentBlockCount(_), _) => stay @@ -1490,7 +1493,10 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } def handleLocalError(cause: Throwable, d: HasCommitments, msg: Option[Any]) = { - log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + cause match { + case _: ForcedLocalCommit => log.warning(s"force-closing channel at user request") + case _ => log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + } cause match { case _: ChannelException => () case _ => log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData ") @@ -1499,6 +1505,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu d match { case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => + log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.txid}") // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) case _ => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index d8125d591..7c0a1aaea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -22,6 +22,7 @@ case class ChannelReserveTooHigh (override val channelId: BinaryDa case class ChannelFundingError (override val channelId: BinaryData) extends ChannelException(channelId, "channel funding error") case class NoMoreHtlcsClosingInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class ClosingAlreadyInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "closing already in progress") +case class CannotCloseInThisState (override val channelId: BinaryData, state: State) extends ChannelException(channelId, s"cannot close in state=$state") case class CannotCloseWithUnsignedOutgoingHtlcs(override val channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs") case class ChannelUnavailable (override val channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)") case class InvalidFinalScript (override val channelId: BinaryData) extends ChannelException(channelId, "invalid final script") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index df3f659c4..6dba96393 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -33,7 +33,6 @@ case object WAIT_FOR_ACCEPT_CHANNEL extends State case object WAIT_FOR_FUNDING_INTERNAL extends State case object WAIT_FOR_FUNDING_CREATED extends State case object WAIT_FOR_FUNDING_SIGNED extends State -case object WAIT_FOR_FUNDING_PUBLISHED extends State case object WAIT_FOR_FUNDING_CONFIRMED extends State case object WAIT_FOR_FUNDING_LOCKED extends State case object NORMAL extends State @@ -63,7 +62,6 @@ case object ERR_INFORMATION_LEAK extends State case class INPUT_INIT_FUNDER(temporaryChannelId: BinaryData, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, fundingTxFeeratePerKw: Long, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelFlags: Byte) case class INPUT_INIT_FUNDEE(temporaryChannelId: BinaryData, localParams: LocalParams, remote: ActorRef, remoteInit: Init) case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close -case object INPUT_PUBLISH_LOCALCOMMIT // used in tests case object INPUT_DISCONNECTED case class INPUT_RECONNECTED(remote: ActorRef) case class INPUT_RESTORED(data: HasCommitments) @@ -97,11 +95,12 @@ final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = fal final case class CMD_FAIL_HTLC(id: Long, reason: Either[BinaryData, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: BinaryData, failureCode: Int, commit: Boolean = false) extends Command final case class CMD_UPDATE_FEE(feeratePerKw: Long, commit: Boolean = false) extends Command -case object CMD_SIGN extends Command +final case object CMD_SIGN extends Command final case class CMD_CLOSE(scriptPubKey: Option[BinaryData]) extends Command -case object CMD_GETSTATE extends Command -case object CMD_GETSTATEDATA extends Command -case object CMD_GETINFO extends Command +final case object CMD_FORCECLOSE extends Command +final case object CMD_GETSTATE extends Command +final case object CMD_GETSTATEDATA extends Command +final case object CMD_GETINFO extends Command final case class RES_GETINFO(nodeId: BinaryData, channelId: BinaryData, state: State, data: Data) /* diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 8e9f8fed6..178308274 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -40,7 +40,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp test((alice, alice2bob, bob2alice, alice2blockchain)) } - test("recv FundingSigned with valid signature") { case (alice, alice2bob, bob2alice, alice2blockchain) => + test("recv FundingSigned with valid signature") { case (alice, _, bob2alice, alice2blockchain) => within(30 seconds) { bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) @@ -50,7 +50,7 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp } } - test("recv FundingSigned with invalid signature") { case (alice, alice2bob, bob2alice, alice2blockchain) => + test("recv FundingSigned with invalid signature") { case (alice, alice2bob, _, _) => within(30 seconds) { // sending an invalid sig alice ! FundingSigned("00" * 32, BinaryData("00" * 64)) @@ -59,11 +59,18 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp } } - test("recv CMD_CLOSE") { case (alice, alice2bob, bob2alice, _) => + test("recv CMD_CLOSE") { case (alice, _, _, _) => within(30 seconds) { alice ! CMD_CLOSE(None) awaitCond(alice.stateName == CLOSED) } } + test("recv CMD_FORCECLOSE") { case (alice, _, _, _) => + within(30 seconds) { + alice ! CMD_FORCECLOSE + awaitCond(alice.stateName == CLOSED) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index ff40c2e18..2c132ff1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -1,5 +1,6 @@ package fr.acinq.eclair.channel.states.c +import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Transaction import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -104,10 +105,18 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH } } - test("recv CMD_CLOSE") { case (alice, _, _, _, alice2blockchain) => + test("recv CMD_CLOSE") { case (alice, _, _, _, _) => + within(30 seconds) { + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(CannotCloseInThisState(channelId(alice), WAIT_FOR_FUNDING_CONFIRMED))) + } + } + + test("recv CMD_FORCECLOSE") { case (alice, _, _, _, alice2blockchain) => within(30 seconds) { val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! CMD_CLOSE(None) + alice ! CMD_FORCECLOSE awaitCond(alice.stateName == CLOSING) alice2blockchain.expectMsg(PublishAsap(tx)) alice2blockchain.expectMsgType[PublishAsap] // claim-main-delayed diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index c4b1a7e17..9edbe5894 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -1,5 +1,7 @@ package fr.acinq.eclair.channel.states.c +import akka.actor.Status +import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Transaction import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -94,10 +96,18 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp } } - test("recv CMD_CLOSE") { case (alice, _, alice2bob, bob2alice, alice2blockchain, router) => + test("recv CMD_CLOSE") { case (alice, _, _, _, _, _) => + within(30 seconds) { + val sender = TestProbe() + sender.send(alice, CMD_CLOSE(None)) + sender.expectMsg(Failure(CannotCloseInThisState(channelId(alice), WAIT_FOR_FUNDING_LOCKED))) + } + } + + test("recv CMD_FORCECLOSE") { case (alice, _, _, _, alice2blockchain, _) => within(30 seconds) { val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.publishableTxs.commitTx.tx - alice ! CMD_CLOSE(None) + alice ! CMD_FORCECLOSE awaitCond(alice.stateName == CLOSING) alice2blockchain.expectMsg(PublishAsap(tx)) alice2blockchain.expectMsgType[PublishAsap] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index e960ec655..4c0afd272 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1863,11 +1863,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { within(30 seconds) { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() - sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === None) + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - assert(relayer.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === None) + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 400000, 42)) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.localParams import initialState.commitments.remoteParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ff4722d41..157934fd9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -3,8 +3,9 @@ package fr.acinq.eclair.channel.states.h import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{OutPoint, ScriptFlags, Transaction, TxIn} -import fr.acinq.eclair.TestkitBaseClass +import fr.acinq.eclair.{Globals, TestkitBaseClass} 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.{CommandBuffer, ForwardAdd, ForwardFulfill, Local} @@ -101,7 +102,6 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } - test("recv CMD_FULFILL_HTLC (unexisting htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => within(30 seconds) { mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) @@ -115,6 +115,36 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } + test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => + within(30 seconds) { + val sender = TestProbe() + // alice initiates a closing + sender.send(alice, CMD_CLOSE(None)) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + // agreeing on a closing fee + val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis + Globals.feeratesPerKw.set(FeeratesPerKw.single(100)) + alice2bob.forward(bob) + val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis + bob2alice.forward(alice) + // they don't converge yet, but alice has a publishable commit tx now + assert(aliceCloseFee != bobCloseFee) + val Some(mutualCloseTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + // let's make alice publish this closing tx + alice ! Error("00" * 32, "") + awaitCond(alice.stateName == CLOSING) + assert(mutualCloseTx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) + + // actual test starts here + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx), 0, 0) + awaitCond(alice.stateName == CLOSED) + } + } + test("recv BITCOIN_TX_CONFIRMED (mutual close)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _, _) => within(30 seconds) { mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index bcd025bb5..d73cce982 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -405,7 +405,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit sender.expectMsgType[State] == OFFLINE }, max = 20 seconds, interval = 1 second) // we then have C unilateral close the channel (which will make F redeem the htlc onchain) - sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) + sender.send(nodes("C").register, Forward(htlc.channelId, CMD_FORCECLOSE)) + sender.expectMsg("ok") // we then wait for F to detect the unilateral close and go to CLOSING state awaitCond({ sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE)) @@ -474,7 +475,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit sender.expectMsgType[State] == OFFLINE }, max = 20 seconds, interval = 1 second) // then we have F unilateral close the channel - sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) + sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FORCECLOSE)) + sender.expectMsg("ok") // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we then generate one block so that the htlc success tx gets written to the blockchain @@ -580,7 +582,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit val res = sender.expectMsgType[JValue](10 seconds) val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString]) // then we ask F to unilaterally close the channel - sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT)) + sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_FORCECLOSE)) + sender.expectMsg("ok") // we then generate enough blocks to make the htlc timeout sender.send(bitcoincli, BitcoinReq("generate", 11)) sender.expectMsgType[JValue](10 seconds) diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 148aceed9..707169bbd 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -5,7 +5,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-SNAPSHOT + 0.2-android-alpha13 eclair-node_2.11 diff --git a/eclair-node/src/main/resources/application.conf b/eclair-node/src/main/resources/application.conf index 476eba17e..b1e3bdceb 100644 --- a/eclair-node/src/main/resources/application.conf +++ b/eclair-node/src/main/resources/application.conf @@ -1,4 +1,5 @@ akka { + loggers = ["akka.event.slf4j.Slf4jLogger"] loglevel = "INFO" diff --git a/eclair-node/src/main/resources/logback_colors.xml b/eclair-node/src/main/resources/logback_colors.xml index e71173dbe..dc3ada972 100644 --- a/eclair-node/src/main/resources/logback_colors.xml +++ b/eclair-node/src/main/resources/logback_colors.xml @@ -81,7 +81,7 @@ - + diff --git a/pom.xml b/pom.xml index 00dc008d2..3c80613fe 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ fr.acinq.eclair eclair_2.11 - 0.2-android-SNAPSHOT + 0.2-android-alpha13 pom