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 6168dfb51..510326d11 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 @@ -367,6 +367,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu val error = Error(d.channelId, "Funding tx timed out".getBytes) goto(CLOSED) sending error + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel => + log.info(s"received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay + case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(d) @@ -390,6 +397,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, None, None, None)) + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => + log.info(s"received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay + case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, _), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(d) @@ -643,21 +657,22 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex), d: DATA_NORMAL) if d.commitments.announceChannel && d.shortChannelId.isEmpty => val shortChannelId = toShortId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt) log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex, sending announcements") - // TODO: empty features - val features = BinaryData("") - val (localNodeSig, localBitcoinSig) = Announcements.signChannelAnnouncement(nodeParams.chainHash, shortChannelId, nodeParams.privateKey, remoteNodeId, d.commitments.localParams.fundingPrivKey, d.commitments.remoteParams.fundingPubKey, features) - val annSignatures = AnnouncementSignatures(d.channelId, shortChannelId, localNodeSig, localBitcoinSig) - stay using d.copy(localAnnouncementSignatures = Some(annSignatures)) sending annSignatures + val annSignatures = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId) + stay using store(d.copy(localAnnouncementSignatures = Some(annSignatures))) sending annSignatures - case Event(remoteAnnSigs: AnnouncementSignatures, d@DATA_NORMAL(commitments, None, _, _, _)) if d.commitments.announceChannel => - // announce channels only if we want to and our peer too - // we would already have closed the connection if we require channels to be announced (even feature bit) but our - // peer does not want channels to be announced + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel => + // channels are publicly announced if both parties want it (defined as feature bit) d.localAnnouncementSignatures match { + case Some(localAnnSigs) if d.shortChannelId.isDefined => + // this can happen if our announcement_signatures was lost during a disconnection + // specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message" + // current implementation always replies to announcement_signatures, not only the first time + log.info(s"re-sending our announcement sigs") + stay sending localAnnSigs case Some(localAnnSigs) => require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") log.info(s"announcing channelId=${d.channelId} on the network with shortId=${localAnnSigs.shortChannelId}") - import commitments.{localParams, remoteParams} + import d.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, localAnnSigs.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth) @@ -671,10 +686,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu context.system.scheduler.scheduleOnce(3 seconds, router, 'tick_broadcast) context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, localAnnSigs.shortChannelId)) // we acknowledge our AnnouncementSignatures message - stay using store(d.copy(shortChannelId = Some(localAnnSigs.shortChannelId), localAnnouncementSignatures = None)) + stay using store(d.copy(shortChannelId = Some(localAnnSigs.shortChannelId))) // note: we don't clear our announcement sigs because we may need to re-send them case None => log.info(s"received remote announcement signatures, delaying") // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message + // note: no need to persist their message, in case of disconnection they will resend it context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) if (nodeParams.spv) { log.warning(s"HACK: since we cannot get the tx index in spv mode, we copy the value sent by remote") @@ -1103,12 +1119,17 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu forwarder ! localShutdown } - // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE + // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been + // fired while we were in OFFLINE (if not, the operation is idempotent anyway) // NB: in spv mode we currently can't get the tx index in block (which is used to calculate the short id) // instead, we rely on a hack by trusting the index the counterparty sends us - if (d.commitments.announceChannel && d.shortChannelId.isEmpty && !nodeParams.spv) { + if (d.commitments.announceChannel && d.localAnnouncementSignatures.isEmpty && !nodeParams.spv) { blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) } + // rfc: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message + if (d.localAnnouncementSignatures.isDefined && d.shortChannelId.isEmpty) { + forwarder ! d.localAnnouncementSignatures.get + } d.shortChannelId.map { case shortChannelId => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 6577e8974..9ec69e58a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -1,14 +1,15 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256} +import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.eclair.blockchain.wallet.EclairWallet import fr.acinq.eclair.crypto.Generators +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ -import fr.acinq.eclair.wire.{ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{Globals, NodeParams} import grizzled.slf4j.Logging @@ -73,6 +74,13 @@ object Helpers { remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio } + def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: Long) = { + // TODO: empty features + val features = BinaryData("") + val (localNodeSig, localBitcoinSig) = Announcements.signChannelAnnouncement(nodeParams.chainHash, shortChannelId, nodeParams.privateKey, commitments.remoteParams.nodeId, commitments.localParams.fundingPrivKey, commitments.remoteParams.fundingPubKey, features) + AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) + } + def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = { import scala.concurrent.duration._ val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds) 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 87b6b23c8..3ba4e0e83 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 @@ -1639,27 +1639,46 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } - test("recv BITCOIN_FUNDING_DEEPLYBURIED", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => + test("recv BITCOIN_FUNDING_DEEPLYBURIED", Tag("channels_public")) { case (alice, _, alice2bob, _, _, _, _) => within(30 seconds) { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - val ann = alice2bob.expectMsgType[AnnouncementSignatures] - assert(alice.stateData.asInstanceOf[DATA_NORMAL] === initialState.copy(localAnnouncementSignatures = Some(ann))) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + assert(alice.stateData.asInstanceOf[DATA_NORMAL] === initialState.copy(localAnnouncementSignatures = Some(annSigs))) } } - test("recv AnnouncementSignatures", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) => + test("recv AnnouncementSignatures", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, _, _, _) => within(30 seconds) { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - val annA = alice2bob.expectMsgType[AnnouncementSignatures] + val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) - val annB = bob2alice.expectMsgType[AnnouncementSignatures] + val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] // actual test starts here bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annB.shortChannelId))) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA))) + } + } + + test("recv AnnouncementSignatures (re-send)", Tag("channels_public")) { case (alice, bob, alice2bob, bob2alice, _, _, _) => + within(30 seconds) { + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) + val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] + sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) + val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA))) + + // actual test starts here + // simulate bob re-sending its sigs + bob2alice.send(alice, annSigsA) + // alice re-sends her sigs + alice2bob.expectMsg(annSigsA) } }