diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala index 56d6d492d8..25f38e5174 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -39,7 +39,7 @@ case class UnspecifiedShortChannelId(private val id: Long) extends ShortChannelI override def toLong: Long = id override def toString: String = toCoordinatesString // for backwards compatibility, because ChannelUpdate have an unspecified scid } -case class RealShortChannelId private (private val id: Long) extends ShortChannelId { +case class RealShortChannelId private(private val id: Long) extends ShortChannelId { override def toLong: Long = id override def toString: String = toCoordinatesString def blockHeight: BlockHeight = ShortChannelId.blockHeight(this) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 37c31a368a..c9331c651a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -20,18 +20,17 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.Monitoring.Metrics import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog import fr.acinq.eclair.wire.protocol.ChannelAnnouncement -import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, TimestampSecond} import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Random, Success} +import scala.util.{Failure, Success} /** * Created by PM on 21/02/2016. @@ -237,11 +236,6 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client case _: WatchConfirmed[_] => // nothing to do case _: WatchFundingLost => // nothing to do } - watches - .collect { - case w: WatchFundingConfirmed if w.minDepth == 0 && w.txId == tx.txid => - checkConfirmed(w) - } Behaviors.same case ProcessNewBlock(blockHash) => @@ -412,21 +406,13 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client client.getTxConfirmations(w.txId).flatMap { case Some(confirmations) if confirmations >= w.minDepth => client.getTransaction(w.txId).flatMap { tx => - w match { - case w: WatchFundingConfirmed if confirmations == 0 => - // if the tx doesn't have confirmations but we don't require any, we reply with a fake block index - // otherwise, we get the real short id - context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(BlockHeight(0), 0, tx)) - Future.successful((): Unit) - case _ => - client.getTransactionShortId(w.txId).map { - case (height, index) => w match { - case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx)) - case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx)) - case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx)) - case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx)) - } - } + client.getTransactionShortId(w.txId).map { + case (height, index) => w match { + case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx)) + case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx)) + case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx)) + case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx)) + } } } case _ => Future.successful((): Unit) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 095d377be3..538f69d9ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -429,10 +429,14 @@ final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, shortIds: ShortIds, lastSent: ChannelReady) extends PersistentChannelData + sealed trait RealScidStatus { def toOption: Option[RealShortChannelId] } object RealScidStatus { + /** The funding transaction has been confirmed but hasn't reached min_depth, we must be ready for a reorg. */ case class Temporary(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** The funding transaction has been deeply confirmed. */ case class Final(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** We don't know the status of the funding transaction. */ case object Unknown extends RealScidStatus { override def toOption: Option[RealShortChannelId] = None } } @@ -441,7 +445,7 @@ object RealScidStatus { * * @param real the real scid, it may change if a reorg happens before the channel reaches 6 conf * @param localAlias we must remember the alias that we sent to our peer because we use it to: - * - identify incoming [[ChannelUpdate]] + * - identify incoming [[ChannelUpdate]] at the connection level * - route outgoing payments to that channel * @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate * routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 910a731407..b41cffaa9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -19,10 +19,10 @@ package fr.acinq.eclair.channel import akka.actor.ActorRef import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} -import fr.acinq.eclair.{BlockHeight, Features, Alias, RealShortChannelId, ShortChannelId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} +import fr.acinq.eclair.{BlockHeight, Features, ShortChannelId} /** * Created by PM on 17/08/2016. @@ -51,15 +51,18 @@ case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, sh case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent { /** - * We always map the local alias because we must always be able to route based on it - * However we only map the real scid if option_scid_alias (TODO: rename to option_scid_privacy) is disabled + * We always include the local alias because we must always be able to route based on it. + * However we only include the real scid if option_scid_alias is disabled, because we otherwise want to hide it. */ def scidsForRouting: Seq[ShortChannelId] = { - commitments match { - case c: Commitments => - val realScid_opt = if (c.channelFeatures.hasFeature(Features.ScidAlias)) None else shortIds.real.toOption - realScid_opt.toSeq :+ shortIds.localAlias - case _ => Seq(shortIds.localAlias) // TODO: ugly + val canUseRealScid = commitments match { + case c: Commitments => !c.channelFeatures.hasFeature(Features.ScidAlias) + case _ => false + } + if (canUseRealScid) { + shortIds.real.toOption.toSeq :+ shortIds.localAlias + } else { + Seq(shortIds.localAlias) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 7bd79bb98e..77b1b15561 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -115,7 +115,7 @@ object ChannelTypes { ).flatten override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat - override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" + override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zero_conf" else ""}" } case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { override def features: Set[InitFeature] = featureBits.activated.keySet 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 a2649ef69e..238b9f4a9e 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 @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} import fr.acinq.eclair.channel.fsm.Channel @@ -35,7 +36,6 @@ import fr.acinq.eclair.transactions.Scripts._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{RealShortChannelId, _} import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -196,17 +196,17 @@ object Helpers { * - before channel announcement: use remote_alias * - after channel announcement: use real scid * - no remote_alias from peer - * - min_depth > 0 : use real scid (may change if reorg between min_depth and 6 conf) - * - min_depth = 0 (zero-conf) : unsupported + * - min_depth > 0: use real scid (may change if reorg between min_depth and 6 conf) + * - min_depth = 0 (zero-conf): spec violation, our peer MUST send an alias when using zero-conf */ - def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], shortIds: ShortIds)(implicit log: DiagnosticLoggingAdapter): ShortChannelId = { + def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], shortIds: ShortIds): ShortChannelId = { channelAnnouncement_opt.map(_.shortChannelId) // we use the real "final" scid when it is publicly announced .orElse(shortIds.remoteAlias_opt) // otherwise the remote alias .orElse(shortIds.real.toOption) // if we don't have a remote alias, we use the real scid (which could change because the funding tx possibly has less than 6 confs here) .getOrElse(throw new RuntimeException("this is a zero-conf channel and no alias was provided in channel_ready")) // if we don't have a real scid, it means this is a zero-conf channel and our peer must have sent an alias } - def scidForChannelUpdate(d: DATA_NORMAL)(implicit log: DiagnosticLoggingAdapter): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds) + def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds) /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by @@ -291,11 +291,11 @@ object Helpers { * wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an * alias in that case). */ - def minDepthFunder(channelFeatures: ChannelFeatures): Long = { + def minDepthFunder(channelFeatures: ChannelFeatures): Option[Long] = { if (channelFeatures.hasFeature(Features.ZeroConf)) { - 0 + None } else { - 1 + Some(1) } } @@ -304,16 +304,16 @@ object Helpers { * we make sure the cumulative block reward largely exceeds the channel size. * * @param fundingSatoshis funding amount of the channel - * @return number of confirmations needed + * @return number of confirmations needed, if any */ - def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Long = fundingSatoshis match { - case _ if channelFeatures.hasFeature(Features.ZeroConf) => 0 // zero-conf stay zero-conf, whatever the funding amount is - case funding if funding <= Channel.MAX_FUNDING => channelConf.minDepthBlocks + def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match { + case _ if channelFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is + case funding if funding <= Channel.MAX_FUNDING => Some(channelConf.minDepthBlocks) case funding => val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 val scalingFactor = 15 val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt - channelConf.minDepthBlocks.max(blocksToReachFunding) + Some(channelConf.minDepthBlocks.max(blocksToReachFunding)) } def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index c385f352ac..7622bd13f1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -49,7 +49,7 @@ class Register extends Actor with ActorLogging { case scidAssigned: ShortChannelIdAssigned => // We map all known scids (real or alias) to the channel_id. The relayer is in charge of deciding whether a real - // scid can be used or not for routing (see option_scid_privacy), but the register is neutral. + // scid can be used or not for routing (see option_scid_alias), but the register is neutral. val m = (scidAssigned.shortIds.real.toOption.toSeq :+ scidAssigned.shortIds.localAlias).map(_ -> scidAssigned.channelId).toMap context become main(channels, shortIds ++ m, channelsTo) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 106a48ad6f..8d8a80e800 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -615,30 +615,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => val finalRealShortId = RealScidStatus.Final(RealShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)) - val shortIds1 = d.shortIds.copy(real = finalRealShortId) log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=${finalRealShortId.realScid}") + val shortIds1 = d.shortIds.copy(real = finalRealShortId) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId)) if (d.shortIds.real == RealScidStatus.Unknown) { // this is a zero-conf channel and it is the first time we know for sure that the funding tx has been confirmed context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) } - if (!d.shortIds.real.toOption.contains(finalRealShortId.realScid)) { - log.info(s"setting final real scid: old=${d.shortIds.real} new=${finalRealShortId}") - // we announce the new shortChannelId - context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId)) - } val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1) // if the shortChannelId is different from the one we had before, we need to re-announce it val channelUpdate1 = if (d.channelUpdate.shortChannelId != scidForChannelUpdate) { log.info(s"using new scid in channel_update: old=${d.channelUpdate.shortChannelId} new=$scidForChannelUpdate") // we re-announce the channelUpdate for the same reason Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - } else d.channelUpdate - val localAnnSigs_opt = if (d.commitments.announceChannel) { + } else { + d.channelUpdate + } + if (d.commitments.announceChannel) { // if channel is public we need to send our announcement_signatures in order to generate the channel_announcement - Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid)) - } else None - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs_opt.toSeq + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs + } else { + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() + } case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel => // channels are publicly announced if both parties want it (defined as feature bit) @@ -1326,7 +1327,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) + require(minDepth.nonEmpty, "min_depth must be set since we're waiting for the funding tx to confirm") + blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth.get) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => @@ -1618,6 +1620,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // We only send the channel_update directly to the peer if we are connected AND the channel hasn't been announced val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match { case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("restore", d, sendToPeer = false)) + case (WAIT_FOR_FUNDING_CONFIRMED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) case (WAIT_FOR_CHANNEL_READY, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("syncing->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) @@ -1628,7 +1631,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } emitEvent_opt.foreach { case EmitLocalChannelUpdate(reason, d, sendToPeer) => - log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=${sendToPeer} realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no")) + log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=$sendToPeer realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no")) val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) context.system.eventStream.publish(lcu) if (sendToPeer) { @@ -1644,7 +1647,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val (stateData, nextStateData) match { // NORMAL->NORMAL, NORMAL->OFFLINE, SYNCING->NORMAL case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2) - // WAIT_FOR_CHANNEL_READY->NORMAL + // WAIT_FOR_FUNDING_CONFIRMED->NORMAL, WAIT_FOR_CHANNEL_READY->NORMAL + case (_: DATA_WAIT_FOR_FUNDING_CONFIRMED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) case (_: DATA_WAIT_FOR_CHANNEL_READY, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) case _ => () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 28fc47e194..db13acfbad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -31,8 +31,8 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.TxOwner import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelReadyTlv, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{BlockHeight, Features, Alias, RealShortChannelId, ShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} +import fr.acinq.eclair.{Features, RealShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -63,8 +63,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { WAIT_FOR_FUNDING_SIGNED| | | funding_signed | |<-------------------------------| - WAIT_FOR_FUNDING_LOCKED| |WAIT_FOR_FUNDING_LOCKED - | funding_locked funding_locked | + WAIT_FOR_CHANNEL_READY| |WAIT_FOR_CHANNEL_READY + | channel_ready channel_ready | |--------------- ---------------| | \/ | | /\ | @@ -89,7 +89,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), - minimumDepth = minimumDepth, + minimumDepth = minimumDepth.getOrElse(0), htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, @@ -256,9 +256,14 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { // NB: we don't send a ChannelSignatureSent for the first commit log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - val fundingMinDepth = Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending Seq(fundingSigned, channelReady) + } } } @@ -296,9 +301,6 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - val fundingMinDepth = Funding.minDepthFunder(commitments.channelFeatures) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - log.info(s"committing txid=${fundingTx.txid}") // we will publish the funding tx only after the channel state has been written to disk because we want to // make sure we first persist the commitment that returns back the funds to us in case of problem @@ -316,7 +318,14 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { } } - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + Funding.minDepthFunder(commitments.channelFeatures) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady calling publishFundingTx() + } } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -345,54 +354,30 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { - case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - if (channelReady.alias_opt.isDefined && - d.commitments.localParams.isInitiator && - !d.commitments.channelFeatures.features.contains(Features.ZeroConf)) { + case Event(remoteChannelReady: ChannelReady, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + if (remoteChannelReady.alias_opt.isDefined && d.commitments.localParams.isInitiator) { log.info("this chanel isn't zero-conf, but we are funder and they sent an early channel_ready with an alias: no need to wait for confirmations") - // we set a new zero-conf watch which will trigger instantly, the original watch will trigger later and be ignored - blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, 0) + // No need to emit ShortChannelIdAssigned: we will emit it when handling their channel_ready in WAIT_FOR_CHANNEL_READY + val (shortIds, localChannelReady) = acceptFundingTx(d.commitments, RealScidStatus.Unknown) + self ! remoteChannelReady + // NB: we will receive a WatchFundingConfirmedTriggered later that will simply be ignored + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(d.commitments, shortIds, localChannelReady) storing() sending localChannelReady } else { log.info("received their channel_ready, deferring message") + stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected } - stay() using d.copy(deferred = Some(channelReady)) // no need to store, they will re-send if we get disconnected case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { case Success(_) => - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + log.info(s"channelId=${d.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) - val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - deferred.foreach(self ! _) - // this is the real scid, it might change when the funding tx gets deeply buried (if there was a reorg in the meantime) - val realShortChannelIdStatus = if (blockHeight == BlockHeight(0)) { - // If we are using zero-conf then the transaction may not have been confirmed yet, that's why the block - // height is zero. In some cases (e.g. we were down for some time) the tx may actually have confirmed, in - // that case we will have a real scid even if the channel is zero-conf - log.info("skipping funding tx confirmation") - RealScidStatus.Unknown - } - else { - log.info(s"channel was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) - RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) - } - // the alias will use in our channel_update message, the goal is to be able to use our channel - // as soon as it reaches NORMAL state, and before it is announced on the network - val localAlias = ShortChannelId.generateLocalAlias() - // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway - val channelReady = ChannelReady( - channelId = commitments.channelId, - nextPerCommitmentPoint = nextPerCommitmentPoint, - tlvStream = TlvStream(ChannelReadyTlv.ShortChannelIdTlv(localAlias)) - ) + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) - // we announce our identifiers as early as we can - val shortIds = ShortIds(real = realShortChannelIdStatus, localAlias = localAlias, remoteAlias_opt = None) - context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortIds, remoteNodeId = remoteNodeId)) - - goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = shortIds, channelReady) storing() sending channelReady + val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) + val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) + deferred.foreach(self ! _) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady case Failure(t) => log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") goto(CLOSED) @@ -444,7 +429,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - goto(NORMAL) using DATA_NORMAL(d.commitments.copy(remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)), shortIds = shortIds1, None, initialChannelUpdate, None, None, None) storing() + goto(NORMAL) using DATA_NORMAL(d.commitments.copy(remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)), shortIds1, None, initialChannelUpdate, None, None, None) storing() case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_CHANNEL_READY) if d.commitments.announceChannel => log.debug("received remote announcement signatures, delaying") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala index f478859c2f..58adea13a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -19,12 +19,12 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} -import fr.acinq.eclair.BlockHeight -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingSpent} +import fr.acinq.eclair.{Alias, BlockHeight, RealShortChannelId, ShortChannelId} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingLost, WatchFundingSpent} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT, FUNDING_TIMEOUT_FUNDEE} import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx -import fr.acinq.eclair.wire.protocol.Error +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReadyTlv, Error, TlvStream} import scala.concurrent.duration.DurationInt import scala.util.{Failure, Success} @@ -59,6 +59,19 @@ trait FundingHandlers extends CommonHandlers { // TODO: implement this? (not needed if we use a reasonable min_depth) //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) } + + def acceptFundingTx(commitments: Commitments, realScidStatus: RealScidStatus): (ShortIds, ChannelReady) = { + blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + // the alias will use in our peer's channel_update message, the goal is to be able to use our channel as soon + // as it reaches NORMAL state, and before it is announced on the network + val shortIds = ShortIds(realScidStatus, ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortIds, remoteNodeId)) + // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway + val channelReady = ChannelReady(commitments.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(shortIds.localAlias))) + (shortIds, channelReady) + } /** * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 8b091bc3db..73c4fcca11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -311,9 +311,7 @@ object Graph { // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. val ageFactor = edge.desc.shortChannelId match { - case real: RealShortChannelId => - val channelBlockHeight = ShortChannelId.coordinates(real).blockHeight - normalize(channelBlockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + case real: RealShortChannelId => normalize(real.blockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) // for local channels or route hints we don't easily have access to the channel block height, but we want to // give them the best score anyway case _: Alias => 1 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index eff86e4813..5755151d33 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -399,10 +399,10 @@ object Router { val scid_opt = shortIds.remoteAlias_opt.orElse(shortIds.real.toOption) // we override the remote update's scid, because it contains either the real scid or our local alias scid_opt.flatMap { scid => - remoteUpdate_opt.map {remoteUpdate => + remoteUpdate_opt.map { remoteUpdate => ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) - } } + } } } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index b81842d00d..b173ceb2a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -81,7 +81,6 @@ object Validation { def handleChannelValidationResponse(d0: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], r: ValidateResult)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - import nodeParams.db.{network => db} import r.c // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement // (the other ones have already been acknowledged as duplicates) @@ -118,15 +117,15 @@ object Validation { } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) // the funding tx has been spent by a transaction that is now confirmed: peer shouldn't send us those remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c))) } else { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c))) } // there may be a record if we have just restarted - db.removeChannel(c.shortChannelId) + nodeParams.db.network.removeChannel(c.shortChannelId) None } // we also reprocess node and channel_update announcements related to the channel that was just analyzed @@ -159,12 +158,11 @@ object Validation { private def addPublicChannel(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - import nodeParams.db.{network => db} val fundingOutputIndex = outputIndex(ann.shortChannelId) val channelId = toLongId(fundingTxid.reverse, fundingOutputIndex) watcher ! WatchExternalChannelSpent(ctx.self, fundingTxid, fundingOutputIndex, ann.shortChannelId) ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(ann, capacity, None, None) :: Nil)) - db.addChannel(ann, fundingTxid, capacity) + nodeParams.db.network.addChannel(ann, fundingTxid, capacity) // if this is a local channel graduating from private to public, we already have data val privChan_opt = d.privateChannels.get(channelId) val pubChan = PublicChannel( @@ -207,7 +205,7 @@ object Validation { rebroadcast = d.rebroadcast.copy( // we rebroadcast the channel to our peers channels = d.rebroadcast.channels + (pubChan.ann -> d.awaiting.getOrElse(pubChan.ann, if (pubChan.nodeId1 == nodeParams.nodeId || pubChan.nodeId2 == nodeParams.nodeId) Seq(LocalGossip) else Nil).toSet), - // those updates are only defined if the channel is was previously an unannounced local channel, we broadcast them + // those updates are only defined if the channel was previously an unannounced local channel, we broadcast them updates = d.rebroadcast.updates ++ rebroadcastUpdates1 ), graphWithBalances = graph1 @@ -225,7 +223,7 @@ object Validation { val lostChannel = d.channels(shortChannelId).ann log.info("funding tx of channelId={} has been spent", shortChannelId) // we need to remove nodes that aren't tied to any channels anymore - val channels1 = d.channels - lostChannel.shortChannelId + val channels1 = d.channels - shortChannelId val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values)) // let's clean the db and send the events log.info("pruning shortChannelId={} (spent)", shortChannelId) @@ -242,12 +240,12 @@ object Validation { db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graphWithBalances = graphWithBalances1) + d.copy(nodes = d.nodes -- lostNodes, channels = channels1, graphWithBalances = graphWithBalances1) } def handleNodeAnnouncement(d: Data, db: NetworkDb, origins: Set[GossipOrigin], n: NodeAnnouncement, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val remoteOrigins = origins flatMap { + val remoteOrigins = origins.flatMap { case r: RemoteGossip if wasStashed => Some(r.peerConnection) case RemoteGossip(peerConnection, _) => @@ -267,7 +265,7 @@ object Validation { remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n))) val origins1 = d.rebroadcast.nodes(n) ++ origins d.copy(rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins1))) - } else if (d.nodes.contains(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) { + } else if (d.nodes.get(n.nodeId).exists(_.timestamp >= n.timestamp)) { log.debug("ignoring {} (duplicate)", n) remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(n))) d @@ -319,10 +317,15 @@ object Validation { log.debug("ignoring {} (pending rebroadcast)", u) sendDecision(origins, GossipDecision.Accepted(u)) val origins1 = d.rebroadcast.updates(u) ++ origins - // NB: we update the channels because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) - d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + update match { + case Left(_) => + // NB: we update the channels because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + case Right(_) => + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1))) + } } else if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) @@ -367,7 +370,7 @@ object Validation { val pc1 = pc.applyChannelUpdate(update) val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.copy(channels = d.channels + (pc.shortChannelId -> pc1), privateChannels = d.privateChannels - pc1.channelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) } case Some(pc: PrivateChannel) => val publicChannel = false @@ -375,7 +378,7 @@ object Validation { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(previous => previous.timestamp >= u.timestamp && previous.shortChannelId == u.shortChannelId)) { // NB: we also check the id because there could be a switch alias->real scid log.debug("ignoring {} (already know same or newer)", u) sendDecision(origins, GossipDecision.Duplicate(u)) d @@ -426,18 +429,17 @@ object Validation { // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) // about that channel. We can ignore this update since we will receive it again - log.info(s"channel shortChannelId=${realShortChannelId} is back from the dead! requesting announcements about this channel") + log.info(s"channel shortChannelId=$realShortChannelId is back from the dead! requesting announcements about this channel") sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) db.removeFromPruned(realShortChannelId) // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not // when we're sending updates to ourselves - origins head match { + origins.head match { case RemoteGossip(peerConnection, remoteNodeId) => val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(realShortChannelId)), TlvStream.empty) d.sync.get(remoteNodeId) match { case Some(sync) if sync.started => // we already have a pending request to that node, let's add this channel to the list and we'll get it later - // TODO: we only request channels with old style channel_query d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) case _ => // otherwise we send the query right away @@ -498,16 +500,20 @@ object Validation { // channel is graduating from private to public // since this is a local channel, we can trust the announcement, no need to go through the full // verification process and make calls to bitcoin core - val commitments = lcu.commitments.asInstanceOf[Commitments] // TODO: ugly! a public channel has to have a real commitment - val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxid = commitments.commitInput.outPoint.txid, capacity = commitments.capacity) + val fundingTxId = lcu.commitments match { + case commitments: Commitments => commitments.commitInput.outPoint.txid + case _ => ByteVector32.Zeroes + } + val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity) // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) db.removeFromPruned(ann.shortChannelId) log.debug("processing channel_update") handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) case None => log.debug("this is a known private channel, processing channel_update privateChannel={}", privateChannel) - // this a known private channel, we update the short ids (may have the remote_alias) - val d1 = d.copy(privateChannels = d.privateChannels + (privateChannel.channelId -> privateChannel.copy(shortIds = lcu.shortIds))) + // this a known private channel, we update the short ids (we now may have the remote_alias) and the balances + val pc1 = privateChannel.copy(shortIds = lcu.shortIds).updateBalances(lcu.commitments) + val d1 = d.copy(privateChannels = d.privateChannels + (privateChannel.channelId -> pc1)) // then we can process the channel_update handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 8d239dd94b..8dfb7410b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -410,7 +410,7 @@ private[channel] object ChannelCodecs3 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x10, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x09, Codecs.DATA_NORMAL_Codec) .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x07, Codecs.DATA_NORMAL_COMPAT_07_Codec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index 90dd3f0a0c..ae6853e51a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -20,11 +20,9 @@ import org.scalatest.funsuite.AnyFunSuite import scala.util.Try - - class ShortChannelIdSpec extends AnyFunSuite { - test("handle values from 0 to 0xffffffffffff") { + test("handle real short channel ids from 0 to 0xffffffffffff") { val expected = Map( TxCoordinates(BlockHeight(0), 0, 0) -> RealShortChannelId(0), TxCoordinates(BlockHeight(42000), 27, 3) -> RealShortChannelId(0x0000a41000001b0003L), @@ -44,7 +42,7 @@ class ShortChannelIdSpec extends AnyFunSuite { assert(RealShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") } - test("parse a short channel it") { + test("parse a short channel id") { assert(ShortChannelId("42000x27x3").toLong == 0x0000a41000001b0003L) } @@ -58,17 +56,18 @@ class ShortChannelIdSpec extends AnyFunSuite { assert(Try(ShortChannelId("42000x")).isFailure) } - test("scids key space") { - + test("compare different types of short channel ids") { val id = 123456 val alias = Alias(id) val realScid = RealShortChannelId(id) val scid = ShortChannelId(id) - + assert(alias == realScid) + assert(realScid == scid) val m = Map(alias -> "alias", realScid -> "real", scid -> "unknown") - // all scids are in the same key space assert(m.size == 1) - + // Values outside of the range [0;0xffffffffffff] can be used for aliases. + Seq(-561L, 0xffffffffffffffffL, 0x2affffffffffffffL).foreach(id => assert(Alias(id) == UnspecifiedShortChannelId(id))) } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 1f01194324..d521d6c5f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -40,14 +40,14 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging test("compute the funding tx min depth according to funding amount") { - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(1)) == 4) - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), ChannelFeatures(), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.50)) == 31) - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.60)) == 32) - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(30)) == 73) - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(50)) == 121) - assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(Features.ZeroConf), Btc(50)) == 0) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(1)).contains(4)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), ChannelFeatures(), Btc(1)).contains(6)) // 4 conf would be enough but we use min-depth=6 + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(6.25)).contains(16)) // we use scaling_factor=15 and a fixed block reward of 6.25BTC + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.50)).contains(31)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.60)).contains(32)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(30)).contains(73)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(50)).contains(121)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(Features.ZeroConf), Btc(50)).isEmpty) } test("compute refresh delay") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 569d276ab3..b25046e630 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -214,6 +214,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } + val eventListener = TestProbe() + systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) @@ -230,14 +233,18 @@ trait ChannelStateTestsBase extends Assertions with Eventually { bob2alice.forward(alice) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) alice2blockchain.expectMsgType[WatchFundingSpent] - val aliceWatchFundingConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) bob2blockchain.expectMsgType[WatchFundingSpent] - val bobWatchFundingConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] - eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! fundingConfirmedEvent(aliceWatchFundingConfirmed, fundingTx) - bob ! fundingConfirmedEvent(bobWatchFundingConfirmed, fundingTx) + val fundingTx = eventListener.expectMsgType[TransactionPublished].tx + if (!channelType.features.contains(Features.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } + eventually(assert(alice.stateName == WAIT_FOR_CHANNEL_READY)) + eventually(assert(bob.stateName == WAIT_FOR_CHANNEL_READY)) alice2blockchain.expectMsgType[WatchFundingLost] bob2blockchain.expectMsgType[WatchFundingLost] alice2bob.expectMsgType[ChannelReady] @@ -260,13 +267,6 @@ trait ChannelStateTestsBase extends Assertions with Eventually { fundingTx } - /** This simulates the behavior of our watcher: it replies to zero-conf watches with a zero block height. */ - def fundingConfirmedEvent(watch: WatchFundingConfirmed, fundingTx: Transaction) = if (watch.minDepth == 0) { - WatchFundingConfirmedTriggered(BlockHeight(0), 0, fundingTx) - } else { - WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - } - def localOrigin(replyTo: ActorRef): Origin.LocalHot = Origin.LocalHot(replyTo, UUID.randomUUID()) def makeCmdAdd(amount: MilliSatoshi, destination: PublicKey, currentBlockHeight: BlockHeight): (ByteVector32, CMD_ADD_HTLC) = { 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 5f0007e698..ddccfb0499 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 @@ -100,10 +100,11 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + // alice doesn't watch for the funding tx to confirm alice2blockchain.expectMsgType[WatchFundingSpent] - val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth == 0) // zeroconf + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectNoMessage(100 millis) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 22834b04ab..c43a8f7ea5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -68,14 +68,17 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu bob2alice.forward(alice) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[WatchFundingSpent] - val aliceWatchFundingConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[WatchFundingSpent] - val bobWatchFundingConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! fundingConfirmedEvent(aliceWatchFundingConfirmed, fundingTx) - bob ! fundingConfirmedEvent(bobWatchFundingConfirmed, fundingTx) + if (!test.tags.contains(ChannelStateTestsTags.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } alice2blockchain.expectMsgType[WatchFundingLost] bob2blockchain.expectMsgType[WatchFundingLost] alice2bob.expectMsgType[ChannelReady] 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 f71bbab9aa..340f7ca24f 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 @@ -17,9 +17,7 @@ package fr.acinq.eclair.channel.states.c import akka.testkit.{TestFSMRef, TestProbe} -import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction} -import fr.acinq.eclair.Features.ZeroConf import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ @@ -28,7 +26,7 @@ import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITC import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, ChannelReadyTlv, Error, FundingCreated, FundingSigned, Init, OpenChannel} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -82,22 +80,21 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF } } - test("recv ChannelReady (funder)") { f => + test("recv ChannelReady (funder, with remote alias)") { f => import f._ // make bob send a ChannelReady msg val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - val channelReady = bob2alice.expectMsgType[ChannelReady] - assert(channelReady.alias_opt.isDefined) + val bobChannelReady = bob2alice.expectMsgType[ChannelReady] + assert(bobChannelReady.alias_opt.isDefined) // test starts here bob2alice.forward(alice) - // alice keeps bob's channel_ready for later processing - eventually { - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReady)) - } - // and alice also creates a zero-conf watch - val aliceWatchFundingConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(aliceWatchFundingConfirmed.minDepth == 0) + // alice stops waiting for confirmations since bob is accepting the channel + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] + assert(aliceChannelReady.alias_opt.nonEmpty) + awaitAssert(assert(alice.stateName == NORMAL)) } test("recv ChannelReady (funder, no remote alias)") { f => @@ -105,8 +102,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF // make bob send a ChannelReady msg val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - val channelReadyNoAlias = bob2alice.expectMsgType[ChannelReady] - .modify(_.tlvStream.records).using(_.filter { case _: ChannelReadyTlv.ShortChannelIdTlv => false; case _ => true }) + val channelReadyNoAlias = bob2alice.expectMsgType[ChannelReady].copy(tlvStream = TlvStream.empty) // test starts here bob2alice.forward(alice, channelReadyNoAlias) // alice keeps bob's channel_ready for later processing @@ -116,23 +112,6 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF alice2blockchain.expectNoMessage() } - test("recv ChannelReady (funder, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => - import f._ - // make bob send a ChannelReady msg - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - val channelReady = bob2alice.expectMsgType[ChannelReady] - assert(channelReady.alias_opt.isDefined) - // test starts here - bob2alice.forward(alice) - // alice keeps bob's channel_ready for later processing - eventually { - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReady)) - } - // and alice also doesn't creates a zero-conf watch because the channel is already zero-conf - alice2blockchain.expectNoMessage() - } - test("recv ChannelReady (fundee)") { f => import f._ // make alice send a ChannelReady msg @@ -165,17 +144,6 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF assert(channelReady.alias_opt.isDefined) } - test("recv WatchFundingConfirmedTriggered (funder, early ChannelReady from fundee)") { f => - import f._ - // the channel isn't zero-conf, but bob (fundee) has sent an early channel_ready so alice created a zero-conf watch - assert(!alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.channelFeatures.hasFeature(ZeroConf)) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - // the zero-conf watch confirms instantly - alice ! WatchFundingConfirmedTriggered(BlockHeight(0), 0, fundingTx) - awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) - } - test("recv WatchFundingConfirmedTriggered (fundee)") { f => import f._ // we create a new listener that registers after alice has published the funding tx 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 7abdf93f85..cd0854c31a 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 @@ -3468,7 +3468,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) - // zero-conf channel : the funding tx isn't confirmed + // zero-conf channel: the funding tx isn't confirmed assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42000), 42, null) val realShortChannelId = RealShortChannelId(BlockHeight(42000), 42, 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 8c93c02966..ff76462c35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -257,7 +257,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(data.shortIds.localAlias == ShortChannelId(123456789L)) assert(data.shortIds.real == RealScidStatus.Temporary(RealShortChannelId(123456789L))) val binMigrated = channelDataCodec.encode(data).require.toHex - assert(binMigrated.startsWith("0010")) // NB: 01 -> 10 + assert(binMigrated.startsWith("000a")) // NB: 01 -> 0a } {