1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 06:47:46 +01:00

Delay announcement_signatures when received early (#217)

* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_LOCKED`
* delay `announcement_signatures` in state `WAIT_FOR_FUNDING_CONFIRMED`
* always re-send our `announcement_signatures` in response to theirs
This commit is contained in:
Pierre-Marie Padiou 2017-11-17 14:52:00 +01:00 committed by GitHub
parent dd642c961d
commit fcb5bf2549
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 70 additions and 22 deletions

View file

@ -367,6 +367,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
val error = Error(d.channelId, "Funding tx timed out".getBytes) val error = Error(d.channelId, "Funding tx timed out".getBytes)
goto(CLOSED) sending error 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, 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) 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)) 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, 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) 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 => 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) 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") log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex, sending announcements")
// TODO: empty features val annSignatures = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)
val features = BinaryData("") stay using store(d.copy(localAnnouncementSignatures = Some(annSignatures))) sending annSignatures
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
case Event(remoteAnnSigs: AnnouncementSignatures, d@DATA_NORMAL(commitments, None, _, _, _)) if d.commitments.announceChannel => case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel =>
// announce channels only if we want to and our peer too // channels are publicly announced if both parties want it (defined as feature bit)
// 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
d.localAnnouncementSignatures match { 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) => case Some(localAnnSigs) =>
require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") 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}") 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 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 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) 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.scheduler.scheduleOnce(3 seconds, router, 'tick_broadcast)
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, localAnnSigs.shortChannelId)) context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, localAnnSigs.shortChannelId))
// we acknowledge our AnnouncementSignatures message // 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 => case None =>
log.info(s"received remote announcement signatures, delaying") 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 // 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) context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs)
if (nodeParams.spv) { if (nodeParams.spv) {
log.warning(s"HACK: since we cannot get the tx index in spv mode, we copy the value sent by remote") 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 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) // 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 // 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) 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 { d.shortChannelId.map {
case shortChannelId => case shortChannelId =>

View file

@ -1,14 +1,15 @@
package fr.acinq.eclair.channel 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.Script._
import fr.acinq.bitcoin.{OutPoint, _} import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.blockchain.wallet.EclairWallet import fr.acinq.eclair.blockchain.wallet.EclairWallet
import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.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 fr.acinq.eclair.{Globals, NodeParams}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
@ -73,6 +74,13 @@ object Helpers {
remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio 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 = { def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = {
import scala.concurrent.duration._ import scala.concurrent.duration._
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds) val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)

View file

@ -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) { within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe() val sender = TestProbe()
sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
val ann = alice2bob.expectMsgType[AnnouncementSignatures] val annSigs = alice2bob.expectMsgType[AnnouncementSignatures]
assert(alice.stateData.asInstanceOf[DATA_NORMAL] === initialState.copy(localAnnouncementSignatures = Some(ann))) 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) { within(30 seconds) {
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val sender = TestProbe() val sender = TestProbe()
sender.send(alice, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10)) 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)) sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
val annB = bob2alice.expectMsgType[AnnouncementSignatures] val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
// actual test starts here // actual test starts here
bob2alice.forward(alice) 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)
} }
} }